From d84c6b47d3bc88106fbc9eed70d14468c345ac31 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Tue, 5 May 2026 16:43:33 -0700 Subject: [PATCH 1/8] Refactor image conversion - Refactored image conversion handling across various modules to use new framework functions for better clarity and maintainability. - Now, there can be multiple possible image converters per frame stopping after the first success. But also, one converter can part of the work (e.g. dowload from GPU) while a later one converts it to the actual requested format. - Replaced direct calls to `convert_image` with `mlt_frame_convert_image` and added checks for the existence of conversion functions. - Improved documentation in YAML files to clarify the purpose of image conversion callbacks. - Added tests to verify the correct propagation of image conversion functions through frames and producers. --- NEWS | 22 ++ src/framework/mlt.vers | 9 + src/framework/mlt_frame.c | 207 ++++++++++++++---- src/framework/mlt_frame.h | 27 ++- src/framework/mlt_tractor.c | 9 +- src/modules/avformat/filter_avcolour_space.c | 6 +- .../avformat/filter_avcolour_space.yml | 2 +- src/modules/core/consumer_multi.c | 36 +-- src/modules/core/filter_crop.c | 6 +- src/modules/core/filter_fieldorder.c | 6 +- src/modules/core/filter_imageconvert.c | 5 +- src/modules/core/filter_imageconvert.yml | 2 +- src/modules/core/filter_mask_start.c | 1 - src/modules/core/link_timeremap.c | 15 +- src/modules/core/loader.ini | 1 + src/modules/core/producer_loader.c | 56 +++-- src/modules/core/transition_luma.c | 29 +-- src/modules/frei0r/filter_frei0r.c | 6 +- src/modules/frei0r/transition_frei0r.c | 12 +- src/modules/gdk/producer_pango.c | 4 +- src/modules/gdk/producer_pixbuf.c | 6 +- src/modules/movit/filter_movit_convert.cpp | 94 ++------ src/modules/movit/filter_movit_convert.yml | 2 +- src/modules/movit/filter_movit_crop.cpp | 4 +- src/modules/openfx/filter_openfx.c | 2 +- src/modules/qt/transition_qtblend.cpp | 11 +- src/modules/xine/filter_deinterlace.c | 8 +- src/modules/xine/link_deinterlace.c | 10 +- src/tests/CMakeLists.txt | 3 +- src/tests/test_frame/test_frame.cpp | 134 +++++++++++- .../test_mod_avformat/test_mod_avformat.cpp | 47 ++++ src/tests/test_tractor/test_tractor.cpp | 32 +++ 32 files changed, 580 insertions(+), 234 deletions(-) diff --git a/NEWS b/NEWS index 1f601d8a88..b100ad20c1 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,28 @@ MLT Release Notes ----------------- +Version 7.40.0 + +Framework + - Added list-based image-conversion callback dispatch on mlt_frame. + Use `mlt_frame_push_convert_image()` to register a converter instead of + setting `frame->convert_image` directly. Multiple converters are tried in + registration order; the first that succeeds wins. New public API: + - `mlt_frame_push_convert_image()` + - `mlt_frame_convert_image()` + - `mlt_frame_next_convert_image()` + - `mlt_frame_has_convert_image()` + - `mlt_frame_copy_convert_image()` + - API behavior change: `mlt_frame_s::convert_image` is now read-only after + init and is permanently set to the dispatcher `mlt_frame_convert_image()`. + External code that set this field directly to a custom function will no + longer have that function called. If you really need that (never heard of + someone who does), clear the frame's "_convert_image_callbacks" property + and use `mlt_frame_push_convert_image()` instead. + - Image converters (`movit.convert`, `avcolor_space`, `imageconvert`) are + now attached data-driven via `loader.ini` key `image_convert`. + + Version 7.38.0 Framework diff --git a/src/framework/mlt.vers b/src/framework/mlt.vers index ffab8d5a31..222484533c 100644 --- a/src/framework/mlt.vers +++ b/src/framework/mlt.vers @@ -687,3 +687,12 @@ MLT_7.36.0 { mlt_color_convert_trc; mlt_profile_is_valid; } MLT_7.34.0; + +MLT_7.40.0 { + global: + mlt_frame_push_convert_image; + mlt_frame_has_convert_image; + mlt_frame_convert_image; + mlt_frame_next_convert_image; + mlt_frame_copy_convert_image; +} MLT_7.34.0; diff --git a/src/framework/mlt_frame.c b/src/framework/mlt_frame.c index 5d35f5b0d9..5a5536cf49 100644 --- a/src/framework/mlt_frame.c +++ b/src/framework/mlt_frame.c @@ -63,6 +63,7 @@ mlt_frame mlt_frame_init(mlt_service service) self->stack_image = mlt_deque_init(); self->stack_audio = mlt_deque_init(); self->stack_service = mlt_deque_init(); + self->convert_image = mlt_frame_convert_image; } return self; @@ -423,8 +424,8 @@ static int generate_test_image(mlt_properties properties, mlt_frame_get_aspect_ratio(test_frame)); mlt_properties_set_int(properties, "width", *width); mlt_properties_set_int(properties, "height", *height); - if (test_frame->convert_image && requested_format != mlt_image_none) - test_frame->convert_image(test_frame, buffer, format, requested_format); + if (mlt_frame_has_convert_image(test_frame) && requested_format != mlt_image_none) + mlt_frame_convert_image(test_frame, buffer, format, requested_format); mlt_properties_set_int(properties, "format", *format); } } else { @@ -516,8 +517,8 @@ int mlt_frame_get_image(mlt_frame self, if (!error && buffer && *buffer) { mlt_properties_set_int(properties, "width", *width); mlt_properties_set_int(properties, "height", *height); - if (self->convert_image && requested_format != mlt_image_none) - self->convert_image(self, buffer, format, requested_format); + if (mlt_frame_has_convert_image(self) && requested_format != mlt_image_none) + mlt_frame_convert_image(self, buffer, format, requested_format); mlt_properties_set_int(properties, "format", *format); } else { error = generate_test_image(properties, buffer, format, width, height, writable); @@ -527,8 +528,8 @@ int mlt_frame_get_image(mlt_frame self, *buffer = mlt_properties_get_data(properties, "image", NULL); *width = mlt_properties_get_int(properties, "width"); *height = mlt_properties_get_int(properties, "height"); - if (self->convert_image && *buffer && requested_format != mlt_image_none) { - self->convert_image(self, buffer, format, requested_format); + if (mlt_frame_has_convert_image(self) && *buffer && requested_format != mlt_image_none) { + mlt_frame_convert_image(self, buffer, format, requested_format); mlt_properties_set_int(properties, "format", *format); } } else { @@ -829,6 +830,161 @@ void mlt_frame_close(mlt_frame self) } } +/* ---- Image conversion callback list ---- */ + +#define CONVERT_IMAGE_CALLBACKS "_convert_image_callbacks" +#define CONVERT_IMAGE_IDX "_convert_image_idx" + +static int do_convert_image( + mlt_frame self, uint8_t **image, mlt_image_format *format, mlt_image_format output, int idx) +{ + mlt_properties props = MLT_FRAME_PROPERTIES(self); + mlt_deque list = mlt_properties_get_data(props, CONVERT_IMAGE_CALLBACKS, NULL); + int count = list ? mlt_deque_count(list) : 0; + + while (idx < count) { + mlt_properties_set_int(props, CONVERT_IMAGE_IDX, idx); + mlt_convert_image fn = (mlt_convert_image) mlt_deque_peek(list, idx); + int error = fn(self, image, format, output); + if (!error) + return 0; + // fn may have internally advanced _convert_image_idx via mlt_frame_next_convert_image; + // skip past any indices already tried to avoid double-calling. + int next = mlt_properties_get_int(props, CONVERT_IMAGE_IDX); + idx = (next > idx) ? next + 1 : idx + 1; + } + return 1; +} + +/** Register an image-conversion callback on the frame. + * + * Callbacks are appended to a list and dispatched in order by + * \p mlt_frame_convert_image. If \p convert is already registered on this + * frame it is silently ignored (deduplication). Has no effect if either + * argument is NULL. + * + * \public \memberof mlt_frame_s + * \param self a frame + * \param convert the conversion callback to register + */ +void mlt_frame_push_convert_image(mlt_frame self, mlt_convert_image convert) +{ + if (!self || !convert) + return; + mlt_properties props = MLT_FRAME_PROPERTIES(self); + mlt_deque list = mlt_properties_get_data(props, CONVERT_IMAGE_CALLBACKS, NULL); + if (!list) { + list = mlt_deque_init(); + mlt_properties_set_data(props, + CONVERT_IMAGE_CALLBACKS, + list, + 0, + (mlt_destructor) mlt_deque_close, + NULL); + } + // Deduplicate: don't register the same function pointer twice. + for (int i = 0; i < mlt_deque_count(list); i++) + if (mlt_deque_peek(list, i) == (void *) convert) + return; + mlt_deque_push_back(list, (void *) convert); +} + +/** Determine whether any image-conversion callbacks are registered. + * + * \public \memberof mlt_frame_s + * \param self a frame + * \return non-zero if at least one callback has been pushed, zero otherwise + */ +int mlt_frame_has_convert_image(mlt_frame self) +{ + if (!self) + return 0; + mlt_deque list = mlt_properties_get_data(MLT_FRAME_PROPERTIES(self), + CONVERT_IMAGE_CALLBACKS, + NULL); + return list && mlt_deque_count(list) > 0; +} + +/** Convert the frame image to the requested format. + * + * Dispatches the registered conversion callbacks in order, stopping at the + * first one that succeeds (returns 0). If no callback succeeds, returns 1. + * This is also the function stored in the \p convert_image field of every + * frame; external callers that hold a pointer to that field will therefore + * invoke this dispatcher transparently. + * + * \public \memberof mlt_frame_s + * \param self a frame + * \param[in,out] image the image buffer pointer + * \param[in,out] format the current image format; updated to \p output on success + * \param output the desired image format + * \return 0 on success, 1 if no callback could perform the conversion + */ +int mlt_frame_convert_image(mlt_frame self, + uint8_t **image, + mlt_image_format *format, + mlt_image_format output) +{ + return do_convert_image(self, image, format, output, 0); +} + +/** Advance to the next image-conversion callback and continue dispatching. + * + * Call this from inside a conversion callback to delegate to the next + * registered callback in the list (fallback chaining). The current + * callback's index is read from the frame's internal dispatch state, so + * this must only be called from within an active \p mlt_frame_convert_image + * dispatch. + * + * \public \memberof mlt_frame_s + * \param self a frame + * \param[in,out] image the image buffer pointer + * \param[in,out] format the current image format + * \param output the desired image format + * \return 0 if a subsequent callback succeeded, 1 if none did + */ +int mlt_frame_next_convert_image(mlt_frame self, + uint8_t **image, + mlt_image_format *format, + mlt_image_format output) +{ + int idx = mlt_properties_get_int(MLT_FRAME_PROPERTIES(self), CONVERT_IMAGE_IDX); + return do_convert_image(self, image, format, output, idx + 1); +} + +/** Copy the image-conversion callback list from one frame to another. + * + * Used by the tractor and clone functions to propagate converters attached + * to track or source frames onto merged or cloned frames. If \p dst already + * has a callback list the copy is skipped (first-wins semantics). + * + * \public \memberof mlt_frame_s + * \param dst the frame to copy callbacks onto + * \param src the frame to copy callbacks from + */ +void mlt_frame_copy_convert_image(mlt_frame dst, mlt_frame src) +{ + mlt_deque src_list = mlt_properties_get_data(MLT_FRAME_PROPERTIES(src), + CONVERT_IMAGE_CALLBACKS, + NULL); + if (!src_list || !mlt_deque_count(src_list)) + return; + mlt_deque dst_list = mlt_properties_get_data(MLT_FRAME_PROPERTIES(dst), + CONVERT_IMAGE_CALLBACKS, + NULL); + if (dst_list) + return; + dst_list = mlt_deque_init(); + for (int i = 0; i < mlt_deque_count(src_list); i++) + mlt_deque_push_back(dst_list, mlt_deque_peek(src_list, i)); + mlt_properties_set_data(MLT_FRAME_PROPERTIES(dst), + CONVERT_IMAGE_CALLBACKS, + dst_list, + 0, + (mlt_destructor) mlt_deque_close, + NULL); +} + /***** convenience functions *****/ void mlt_frame_write_ppm(mlt_frame frame) @@ -942,18 +1098,7 @@ mlt_frame mlt_frame_clone(mlt_frame self, int is_deep) 0, NULL, NULL); - mlt_properties_set_data(new_props, - "movit.convert", - mlt_properties_get_data(properties, "movit.convert", NULL), - 0, - NULL, - NULL); - mlt_properties_set_data(new_props, - "_movit cpu_convert", - mlt_properties_get_data(properties, "_movit cpu_convert", NULL), - 0, - NULL, - NULL); + mlt_frame_copy_convert_image(new_frame, self); if (is_deep) { data = mlt_properties_get_data(properties, "audio", &size); @@ -1045,18 +1190,7 @@ mlt_frame mlt_frame_clone_audio(mlt_frame self, int is_deep) 0, NULL, NULL); - mlt_properties_set_data(new_props, - "movit.convert", - mlt_properties_get_data(properties, "movit.convert", NULL), - 0, - NULL, - NULL); - mlt_properties_set_data(new_props, - "_movit cpu_convert", - mlt_properties_get_data(properties, "_movit cpu_convert", NULL), - 0, - NULL, - NULL); + mlt_frame_copy_convert_image(new_frame, self); if (is_deep) { data = mlt_properties_get_data(properties, "audio", &size); @@ -1117,18 +1251,7 @@ mlt_frame mlt_frame_clone_image(mlt_frame self, int is_deep) 0, NULL, NULL); - mlt_properties_set_data(new_props, - "movit.convert", - mlt_properties_get_data(properties, "movit.convert", NULL), - 0, - NULL, - NULL); - mlt_properties_set_data(new_props, - "_movit cpu_convert", - mlt_properties_get_data(properties, "_movit cpu_convert", NULL), - 0, - NULL, - NULL); + mlt_frame_copy_convert_image(new_frame, self); if (is_deep) { data = mlt_properties_get_data(properties, "image", &size); diff --git a/src/framework/mlt_frame.h b/src/framework/mlt_frame.h index 764c2f2779..889b2130b8 100644 --- a/src/framework/mlt_frame.h +++ b/src/framework/mlt_frame.h @@ -3,7 +3,7 @@ * \brief interface for all frame classes * \see mlt_frame_s * - * Copyright (C) 2003-2023 Meltytech, LLC + * Copyright (C) 2003-2026 Meltytech, LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -52,6 +52,16 @@ typedef int (*mlt_get_audio)(mlt_frame self, int *channels, int *samples); +/** Callback function to convert image format. + * + * Registered converters are tried in order. Return non-zero to indicate the + * conversion was not handled so the next registered converter will be tried. + */ +typedef int (*mlt_convert_image)(mlt_frame self, + uint8_t **image, + mlt_image_format *input, + mlt_image_format output); + /** \brief Frame class * * The frame is the primary data object that gets passed around to and through services. @@ -91,7 +101,9 @@ struct mlt_frame_s { struct mlt_properties_s parent; /**< \private A frame extends properties. */ - /** Convert the image format (callback function). + /** Image format conversion dispatcher (read-only after init). + * Always set to mlt_frame_convert_image by mlt_frame_init(). + * Do not set this field directly; use mlt_frame_push_convert_image() instead. * \param self a frame * \param[in,out] image a buffer of image data * \param[in,out] input the image format of supplied image data @@ -171,6 +183,17 @@ MLT_EXPORT mlt_producer mlt_frame_get_original_producer(mlt_frame self); MLT_EXPORT void mlt_frame_close(mlt_frame self); MLT_EXPORT mlt_properties mlt_frame_unique_properties(mlt_frame self, mlt_service service); MLT_EXPORT mlt_properties mlt_frame_get_unique_properties(mlt_frame self, mlt_service service); +MLT_EXPORT void mlt_frame_push_convert_image(mlt_frame self, mlt_convert_image convert); +MLT_EXPORT int mlt_frame_has_convert_image(mlt_frame self); +MLT_EXPORT int mlt_frame_convert_image(mlt_frame self, + uint8_t **image, + mlt_image_format *format, + mlt_image_format output); +MLT_EXPORT int mlt_frame_next_convert_image(mlt_frame self, + uint8_t **image, + mlt_image_format *format, + mlt_image_format output); +MLT_EXPORT void mlt_frame_copy_convert_image(mlt_frame dst, mlt_frame src); MLT_EXPORT mlt_frame mlt_frame_clone(mlt_frame self, int is_deep); MLT_EXPORT mlt_frame mlt_frame_clone_audio(mlt_frame self, int is_deep); MLT_EXPORT mlt_frame mlt_frame_clone_image(mlt_frame self, int is_deep); diff --git a/src/framework/mlt_tractor.c b/src/framework/mlt_tractor.c index fef6e38dc0..a8527ca3cf 100644 --- a/src/framework/mlt_tractor.c +++ b/src/framework/mlt_tractor.c @@ -3,7 +3,7 @@ * \brief tractor service class * \see mlt_tractor_s * - * Copyright (C) 2003-2022 Meltytech, LLC + * Copyright (C) 2003-2026 Meltytech, LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -418,8 +418,8 @@ static int producer_get_image(mlt_frame self, if (data) { mlt_frame_set_alpha(self, data, size, NULL); } - self->convert_image = frame->convert_image; self->convert_audio = frame->convert_audio; + mlt_frame_copy_convert_image(self, frame); return 0; } @@ -537,11 +537,10 @@ static int producer_get_frame(mlt_producer parent, mlt_frame_ptr frame, int trac subtitle_properties); } - // Copy the format conversion virtual functions - if (!(*frame)->convert_image && temp->convert_image) - (*frame)->convert_image = temp->convert_image; + // Copy the format conversion callbacks if (!(*frame)->convert_audio && temp->convert_audio) (*frame)->convert_audio = temp->convert_audio; + mlt_frame_copy_convert_image(*frame, temp); // Check for last track done = mlt_properties_get_int(temp_properties, "last_track"); diff --git a/src/modules/avformat/filter_avcolour_space.c b/src/modules/avformat/filter_avcolour_space.c index c6b898ee31..f04d7cf7c2 100644 --- a/src/modules/avformat/filter_avcolour_space.c +++ b/src/modules/avformat/filter_avcolour_space.c @@ -1,6 +1,6 @@ /* * filter_avcolour_space.c -- Colour space filter - * Copyright (C) 2004-2025 Meltytech, LLC + * Copyright (C) 2004-2026 Meltytech, LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -355,9 +355,7 @@ static int convert_image(mlt_frame frame, static mlt_frame filter_process(mlt_filter filter, mlt_frame frame) { - if (!frame->convert_image) - frame->convert_image = convert_image; - + mlt_frame_push_convert_image(frame, convert_image); return frame; } diff --git a/src/modules/avformat/filter_avcolour_space.yml b/src/modules/avformat/filter_avcolour_space.yml index 7b31d3f8ab..6e0b3c6d43 100644 --- a/src/modules/avformat/filter_avcolour_space.yml +++ b/src/modules/avformat/filter_avcolour_space.yml @@ -13,4 +13,4 @@ tags: description: Converts the colorspace and pixel format. notes: > This is not intended to be created directly. Rather, the loader producer - loads it if it is available to set the convert_image function pointer on frames. + loads it if it is available to register an image conversion callback on frames. diff --git a/src/modules/core/consumer_multi.c b/src/modules/core/consumer_multi.c index 3c6c011db3..c095b3800d 100644 --- a/src/modules/core/consumer_multi.c +++ b/src/modules/core/consumer_multi.c @@ -122,6 +122,9 @@ static void attach_normalizers(mlt_profile profile, mlt_service service) // Apply normalizers for (i = 0; i < mlt_properties_count(normalizers); i++) { + const char *key = mlt_properties_get_name(normalizers, i); + if (!key || !strcmp(key, "image_convert")) + continue; char *value = mlt_properties_get_value(normalizers, i); mlt_tokeniser_parse_new(tokeniser, value, ","); int created = 0; @@ -136,20 +139,27 @@ static void attach_normalizers(mlt_profile profile, mlt_service service) // Close the tokeniser mlt_tokeniser_close(tokeniser); - // Attach the audio and video format converters - int created = 0; - // movit.convert skips setting the frame->convert_image pointer if GLSL cannot be used. - mlt_filter filter = mlt_factory_filter(profile, "movit.convert", NULL); - if (filter != NULL) { - mlt_properties_set_int(MLT_FILTER_PROPERTIES(filter), "_loader", 1); - mlt_service_attach(service, filter); - mlt_filter_close(filter); - created = 1; + // Attach image converters from loader.ini image_convert (all listed, in order). + { + char *value = mlt_properties_get(normalizers, "image_convert"); + if (value) { + mlt_tokeniser tokeniser2 = mlt_tokeniser_init(); + mlt_tokeniser_parse_new(tokeniser2, value, ","); + for (int j = 0; j < mlt_tokeniser_count(tokeniser2); j++) { + char *name = mlt_tokeniser_get_string(tokeniser2, j); + if (name) { + mlt_filter f = mlt_factory_filter(profile, name, NULL); + if (f) { + mlt_properties_set_int(MLT_FILTER_PROPERTIES(f), "_loader", 1); + mlt_service_attach(service, f); + mlt_filter_close(f); + } + } + } + mlt_tokeniser_close(tokeniser2); + } } - // avcolor_space and imageconvert only set frame->convert_image if it has not been set. - create_filter(profile, service, "avcolor_space", &created); - if (!created) - create_filter(profile, service, "imageconvert", &created); + int created = 0; create_filter(profile, service, "audioconvert", &created); } diff --git a/src/modules/core/filter_crop.c b/src/modules/core/filter_crop.c index 859ffea742..2569201eca 100644 --- a/src/modules/core/filter_crop.c +++ b/src/modules/core/filter_crop.c @@ -1,6 +1,6 @@ /* * filter_crop.c -- cropping filter - * Copyright (C) 2009-2022 Meltytech, LLC + * Copyright (C) 2009-2026 Meltytech, LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -108,8 +108,8 @@ static int filter_get_image(mlt_frame frame, requested_format = mlt_image_rgb; } - if (*format != requested_format && frame->convert_image) { - frame->convert_image(frame, image, format, requested_format); + if (*format != requested_format && mlt_frame_has_convert_image(frame)) { + mlt_frame_convert_image(frame, image, format, requested_format); } mlt_log_debug(NULL, diff --git a/src/modules/core/filter_fieldorder.c b/src/modules/core/filter_fieldorder.c index 2a34b1b3c0..d22745017e 100644 --- a/src/modules/core/filter_fieldorder.c +++ b/src/modules/core/filter_fieldorder.c @@ -1,6 +1,6 @@ /* * filter_fieldorder.c -- change field dominance - * Copyright (C) 2011-2019 Meltytech, LLC + * Copyright (C) 2011-2026 Meltytech, LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -54,8 +54,8 @@ static int get_image(mlt_frame frame, && mlt_properties_get(properties, "progressive") && mlt_properties_get_int(properties, "progressive") == 0) { // We only work with non-planar formats - if (*format == mlt_image_yuv420p && frame->convert_image) - error = frame->convert_image(frame, image, format, mlt_image_yuv422); + if (*format == mlt_image_yuv420p && mlt_frame_has_convert_image(frame)) + error = mlt_frame_convert_image(frame, image, format, mlt_image_yuv422); // Make a new image int bpp; diff --git a/src/modules/core/filter_imageconvert.c b/src/modules/core/filter_imageconvert.c index 8e3e820aee..4f5ed5d283 100644 --- a/src/modules/core/filter_imageconvert.c +++ b/src/modules/core/filter_imageconvert.c @@ -1,6 +1,6 @@ /* * filter_imageconvert.c -- colorspace and pixel format converter - * Copyright (C) 2009-2021 Meltytech, LLC + * Copyright (C) 2009-2026 Meltytech, LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -493,8 +493,7 @@ static int convert_image(mlt_frame frame, static mlt_frame filter_process(mlt_filter filter, mlt_frame frame) { - if (!frame->convert_image) - frame->convert_image = convert_image; + mlt_frame_push_convert_image(frame, convert_image); return frame; } diff --git a/src/modules/core/filter_imageconvert.yml b/src/modules/core/filter_imageconvert.yml index 6ea94f8de5..ddae1b258f 100644 --- a/src/modules/core/filter_imageconvert.yml +++ b/src/modules/core/filter_imageconvert.yml @@ -11,6 +11,6 @@ tags: description: Converts the colorspace and pixel format. notes: > This is not intended to be created directly. Rather, the loader producer - loads it if it is available to set the convert_image function pointer on frames. + loads it if it is available to register an image conversion callback on frames. This implementation is old and naive by assuming all YCbCr video is ITU-R BT.601 and all RGB is sRGB. diff --git a/src/modules/core/filter_mask_start.c b/src/modules/core/filter_mask_start.c index bd5135d6ed..407fef44ec 100644 --- a/src/modules/core/filter_mask_start.c +++ b/src/modules/core/filter_mask_start.c @@ -36,7 +36,6 @@ static int get_image(mlt_frame frame, if (!error && !mlt_properties_exists(properties, "mask frame")) { mlt_frame clone = mlt_frame_clone(frame, 1); clone->convert_audio = frame->convert_audio; - clone->convert_image = frame->convert_image; mlt_properties_set_data(properties, "mask frame", clone, diff --git a/src/modules/core/link_timeremap.c b/src/modules/core/link_timeremap.c index ef554e3297..e3a3e3abad 100644 --- a/src/modules/core/link_timeremap.c +++ b/src/modules/core/link_timeremap.c @@ -1,6 +1,6 @@ /* * link_timeremap.c - * Copyright (C) 2020-2025 Meltytech, LLC + * Copyright (C) 2020-2026 Meltytech, LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -629,18 +629,7 @@ static int link_get_frame(mlt_link self, mlt_frame_ptr frame, int index) // Copy some useful properties from one of the source frames. (*frame)->convert_image = src_frame->convert_image; (*frame)->convert_audio = src_frame->convert_audio; - mlt_filter cpu_csc = (mlt_filter) mlt_properties_get_data(MLT_FRAME_PROPERTIES(src_frame), - "_movit cpu_convert", - NULL); - if (cpu_csc) { - mlt_properties_inc_ref(MLT_FILTER_PROPERTIES(cpu_csc)); - mlt_properties_set_data(MLT_FRAME_PROPERTIES(*frame), - "_movit cpu_convert", - cpu_csc, - 0, - (mlt_destructor) mlt_filter_close, - NULL); - } + mlt_frame_copy_convert_image(*frame, src_frame); mlt_properties_pass_list(MLT_FRAME_PROPERTIES(*frame), MLT_FRAME_PROPERTIES(src_frame), "audio_frequency"); diff --git a/src/modules/core/loader.ini b/src/modules/core/loader.ini index ea75da96ad..a753b2e17a 100644 --- a/src/modules/core/loader.ini +++ b/src/modules/core/loader.ini @@ -7,6 +7,7 @@ # the second and third are applied as applicable). # image filters +image_convert=movit.convert,avcolor_space,imageconvert color=color_transform deinterlace=deinterlace,avdeinterlace fieldorder=fieldorder diff --git a/src/modules/core/producer_loader.c b/src/modules/core/producer_loader.c index 64327cc03c..ffe18c2072 100644 --- a/src/modules/core/producer_loader.c +++ b/src/modules/core/producer_loader.c @@ -1,6 +1,6 @@ /* * producer_loader.c -- auto-load producer by file name extension - * Copyright (C) 2003-2023 Meltytech, LLC + * Copyright (C) 2003-2026 Meltytech, LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -195,29 +195,30 @@ static void create_filter(mlt_profile profile, free(id); } -static void attach_normalizers(mlt_profile profile, mlt_producer producer, int nogl) +static void ensure_normalizers_loaded(void) { - // Loop variable - int i; - - // Tokeniser - mlt_tokeniser tokeniser = mlt_tokeniser_init(); - - // We only need to load the normalizing properties once if (normalizers == NULL) { char temp[PATH_MAX]; snprintf(temp, sizeof(temp), "%s/core/loader.ini", mlt_environment("MLT_DATA")); normalizers = mlt_properties_load(temp); mlt_factory_register_for_clean_up(normalizers, (mlt_destructor) mlt_properties_close); } +} + +static void attach_normalizers(mlt_profile profile, mlt_producer producer, int nogl) +{ + mlt_tokeniser tokeniser = mlt_tokeniser_init(); - // Apply normalizers - for (i = 0; i < mlt_properties_count(normalizers); i++) { - int j = 0; + ensure_normalizers_loaded(); + + for (int i = 0; i < mlt_properties_count(normalizers); i++) { + const char *key = mlt_properties_get_name(normalizers, i); + if (!key || !strcmp(key, "image_convert")) + continue; int created = 0; char *value = mlt_properties_get_value(normalizers, i); mlt_tokeniser_parse_new(tokeniser, value, ","); - for (j = 0; !created && j < mlt_tokeniser_count(tokeniser); j++) { + for (int j = 0; !created && j < mlt_tokeniser_count(tokeniser); j++) { const char *filter_name = mlt_tokeniser_get_string(tokeniser, j); if (!nogl || (filter_name && strncmp(filter_name, "movit.", 6))) create_filter(profile, producer, filter_name, &created); @@ -228,6 +229,24 @@ static void attach_normalizers(mlt_profile profile, mlt_producer producer, int n mlt_tokeniser_close(tokeniser); } +static void attach_image_converters(mlt_profile profile, mlt_producer producer, int nogl) +{ + ensure_normalizers_loaded(); + char *value = mlt_properties_get(normalizers, "image_convert"); + if (!value) + return; + mlt_tokeniser tokeniser = mlt_tokeniser_init(); + mlt_tokeniser_parse_new(tokeniser, value, ","); + for (int j = 0; j < mlt_tokeniser_count(tokeniser); j++) { + const char *name = mlt_tokeniser_get_string(tokeniser, j); + if (name && (!nogl || strncmp(name, "movit.", 6) != 0)) { + int created = 0; + create_filter(profile, producer, name, &created); + } + } + mlt_tokeniser_close(tokeniser); +} + mlt_producer producer_loader_init(mlt_profile profile, mlt_service_type type, const char *id, @@ -253,15 +272,10 @@ mlt_producer producer_loader_init(mlt_profile profile, attach_normalizers(profile, producer, nogl); if (producer && mlt_service_identify(MLT_PRODUCER_SERVICE(producer)) != mlt_service_chain_type) { - // Always let the image and audio be converted + // Always let the image and audio be converted. + // Image converters are loaded from loader.ini image_convert in order. + attach_image_converters(profile, producer, nogl); int created = 0; - // movit.convert skips setting the frame->convert_image pointer if GLSL cannot be used. - if (!nogl) - create_filter(profile, producer, "movit.convert", &created); - // avcolor_space and imageconvert only set frame->convert_image if it has not been set. - create_filter(profile, producer, "avcolor_space", &created); - if (!created) - create_filter(profile, producer, "imageconvert", &created); create_filter(profile, producer, "audioconvert", &created); } diff --git a/src/modules/core/transition_luma.c b/src/modules/core/transition_luma.c index 47f164a84e..6f79a3d77e 100644 --- a/src/modules/core/transition_luma.c +++ b/src/modules/core/transition_luma.c @@ -1,6 +1,6 @@ /* * transition_luma.c -- a generic dissolve/wipe processor - * Copyright (C) 2003-2025 Meltytech, LLC + * Copyright (C) 2003-2026 Meltytech, LLC * * Adapted from Kino Plugin Timfx, which is * Copyright (C) 2002 Timothy M. Shead @@ -140,22 +140,23 @@ static inline int dissolve_yuv422(mlt_frame frame, int ret = 0; int i = height + 1; int width_src = width, height_src = height; - mlt_image_format format = (fix_background_alpha && frame->convert_image) ? mlt_image_rgba - : mlt_image_yuv422; + mlt_image_format format = (fix_background_alpha && mlt_frame_has_convert_image(frame)) + ? mlt_image_rgba + : mlt_image_yuv422; uint8_t *p_src, *p_dest; uint8_t *alpha_src; uint8_t *alpha_dst; int mix = weight * (1 << 16); mlt_frame_get_image(frame, &p_dest, &format, &width, &height, 1); - if (fix_background_alpha && frame->convert_image) - frame->convert_image(frame, &p_dest, &format, mlt_image_yuv422); + if (fix_background_alpha && mlt_frame_has_convert_image(frame)) + mlt_frame_convert_image(frame, &p_dest, &format, mlt_image_yuv422); alpha_dst = mlt_frame_get_alpha(frame); - if (fix_background_alpha && that->convert_image) + if (fix_background_alpha && mlt_frame_has_convert_image(that)) format = mlt_image_rgba; mlt_frame_get_image(that, &p_src, &format, &width_src, &height_src, 0); - if (that->convert_image) - that->convert_image(that, &p_src, &format, mlt_image_yuv422); + if (mlt_frame_has_convert_image(that)) + mlt_frame_convert_image(that, &p_src, &format, mlt_image_yuv422); alpha_src = mlt_frame_get_alpha(that); int is_translucent = (alpha_dst && !is_opaque(alpha_dst, width, height)) || (alpha_src && !is_opaque(alpha_src, width_src, height_src)); @@ -339,10 +340,10 @@ static void luma_composite_yuv422(mlt_frame a_frame, { int width_src = *width, height_src = *height; int width_dest = *width, height_dest = *height; - mlt_image_format format_src = (fix_background_alpha && a_frame->convert_image) + mlt_image_format format_src = (fix_background_alpha && mlt_frame_has_convert_image(a_frame)) ? mlt_image_rgba : mlt_image_yuv422; - mlt_image_format format_dest = (fix_background_alpha && b_frame->convert_image) + mlt_image_format format_dest = (fix_background_alpha && mlt_frame_has_convert_image(b_frame)) ? mlt_image_rgba : mlt_image_yuv422; uint8_t *p_src, *p_dest; @@ -356,12 +357,12 @@ static void luma_composite_yuv422(mlt_frame a_frame, "distort", mlt_properties_get(&a_frame->parent, "distort")); mlt_frame_get_image(a_frame, &p_dest, &format_dest, &width_dest, &height_dest, 1); - if (fix_background_alpha && a_frame->convert_image) - a_frame->convert_image(a_frame, &p_dest, &format_dest, mlt_image_yuv422); + if (fix_background_alpha && mlt_frame_has_convert_image(a_frame)) + mlt_frame_convert_image(a_frame, &p_dest, &format_dest, mlt_image_yuv422); alpha_dest = mlt_frame_get_alpha(a_frame); mlt_frame_get_image(b_frame, &p_src, &format_src, &width_src, &height_src, 0); - if (fix_background_alpha && b_frame->convert_image) - b_frame->convert_image(b_frame, &p_src, &format_src, mlt_image_yuv422); + if (fix_background_alpha && mlt_frame_has_convert_image(b_frame)) + mlt_frame_convert_image(b_frame, &p_src, &format_src, mlt_image_yuv422); alpha_src = mlt_frame_get_alpha(b_frame); if (*width == 0 || *height == 0) diff --git a/src/modules/frei0r/filter_frei0r.c b/src/modules/frei0r/filter_frei0r.c index f9e0c3eee1..f6af7b2191 100644 --- a/src/modules/frei0r/filter_frei0r.c +++ b/src/modules/frei0r/filter_frei0r.c @@ -1,7 +1,7 @@ /* * filter_frei0r.c -- frei0r filter * Copyright (c) 2008 Marco Gittler - * Copyright (C) 2009-2025 Meltytech, LLC + * Copyright (C) 2009-2026 Meltytech, LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -80,11 +80,11 @@ static int filter_get_image(mlt_frame frame, int error = mlt_frame_get_image(frame, image, format, width, height, 0); // Now request the same image as rgba using convert_image - if (!error && *image && *format == mlt_image_rgba64 && frame->convert_image) { + if (!error && *image && *format == mlt_image_rgba64 && mlt_frame_has_convert_image(frame)) { // Convert to rgba mlt_frame temp_frame = mlt_frame_clone(frame, 0); uint8_t *rgba64_image = *image; - error = frame->convert_image(temp_frame, image, format, mlt_image_rgba); + error = mlt_frame_convert_image(temp_frame, image, format, mlt_image_rgba); if (!error && *image && *format == mlt_image_rgba) { // Process with frei0r using rgba image mlt_position position = mlt_filter_get_position(filter, frame); diff --git a/src/modules/frei0r/transition_frei0r.c b/src/modules/frei0r/transition_frei0r.c index c9448bc924..1de1535798 100644 --- a/src/modules/frei0r/transition_frei0r.c +++ b/src/modules/frei0r/transition_frei0r.c @@ -1,7 +1,7 @@ /* * transition_frei0r.c -- frei0r transition * Copyright (c) 2008 Marco Gittler - * Copyright (C) 2009-2020 Meltytech, LLC + * Copyright (C) 2009-2026 Meltytech, LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -46,10 +46,11 @@ static int transition_get_image(mlt_frame a_frame, if (error) return error; - if (b_frame->convert_image && (*width != request_width || *height != request_height)) { + if (mlt_frame_has_convert_image(b_frame) + && (*width != request_width || *height != request_height)) { mlt_properties_set_int(b_props, "convert_image_width", request_width); mlt_properties_set_int(b_props, "convert_image_height", request_height); - b_frame->convert_image(b_frame, &images[1], format, *format); + mlt_frame_convert_image(b_frame, &images[1], format, *format); *width = request_width; *height = request_height; } @@ -82,10 +83,11 @@ static int transition_get_image(mlt_frame a_frame, if (error) return error; - if (a_frame->convert_image && (*width != request_width || *height != request_height)) { + if (mlt_frame_has_convert_image(a_frame) + && (*width != request_width || *height != request_height)) { mlt_properties_set_int(a_props, "convert_image_width", request_width); mlt_properties_set_int(a_props, "convert_image_height", request_height); - a_frame->convert_image(a_frame, &images[0], format, *format); + mlt_frame_convert_image(a_frame, &images[0], format, *format); *width = request_width; *height = request_height; } diff --git a/src/modules/gdk/producer_pango.c b/src/modules/gdk/producer_pango.c index b62a457d11..d7b3914437 100644 --- a/src/modules/gdk/producer_pango.c +++ b/src/modules/gdk/producer_pango.c @@ -712,8 +712,8 @@ static int producer_get_image(mlt_frame frame, } // convert image - if (frame->convert_image && cached->format != *format) { - frame->convert_image(frame, &buf, &cached->format, *format); + if (mlt_frame_has_convert_image(frame) && cached->format != *format) { + mlt_frame_convert_image(frame, &buf, &cached->format, *format); *format = cached->format; if (buf != buf_save) mlt_pool_release(buf_save); diff --git a/src/modules/gdk/producer_pixbuf.c b/src/modules/gdk/producer_pixbuf.c index b0a41ab98b..1074fad188 100644 --- a/src/modules/gdk/producer_pixbuf.c +++ b/src/modules/gdk/producer_pixbuf.c @@ -1,6 +1,6 @@ /* * producer_pixbuf.c -- raster image loader based upon gdk-pixbuf - * Copyright (C) 2003-2023 Meltytech, LLC + * Copyright (C) 2003-2026 Meltytech, LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -635,7 +635,7 @@ static void refresh_image( // Convert image to requested format if (format != mlt_image_none && format != mlt_image_movit && format != self->format - && frame->convert_image) { + && mlt_frame_has_convert_image(frame)) { // cache copies of the image and alpha buffers uint8_t *buffer = self->image; if (buffer) { @@ -644,7 +644,7 @@ static void refresh_image( mlt_properties_set_int(properties, "height", self->height); mlt_properties_set_int(properties, "format", self->format); - if (!frame->convert_image(frame, &self->image, &self->format, format)) { + if (!mlt_frame_convert_image(frame, &self->image, &self->format, format)) { buffer = self->image; image_size = mlt_image_format_size(self->format, self->width, self->height, NULL); diff --git a/src/modules/movit/filter_movit_convert.cpp b/src/modules/movit/filter_movit_convert.cpp index 4d047878ec..d674961c85 100644 --- a/src/modules/movit/filter_movit_convert.cpp +++ b/src/modules/movit/filter_movit_convert.cpp @@ -49,31 +49,6 @@ static void yuv422_to_yuv422p(uint8_t *yuv422, uint8_t *yuv422p, int width, int } } -static int convert_on_cpu(mlt_frame frame, - uint8_t **image, - mlt_image_format *format, - mlt_image_format output_format) -{ - int error = 0; - mlt_filter cpu_csc = (mlt_filter) mlt_properties_get_data(MLT_FRAME_PROPERTIES(frame), - "_movit cpu_convert", - NULL); - if (cpu_csc) { - int (*save_fp)(mlt_frame self, - uint8_t * *image, - mlt_image_format * input, - mlt_image_format output) - = frame->convert_image; - frame->convert_image = NULL; - mlt_filter_process(cpu_csc, frame); - error = frame->convert_image(frame, image, format, output_format); - frame->convert_image = save_fp; - } else { - error = 1; - } - return error; -} - static void delete_chain(EffectChain *chain) { delete chain; @@ -495,7 +470,7 @@ static int movit_render(EffectChain *chain, error = glsl->render_frame_ycbcr(chain, frame, width, height, image); if (!error && output_format != mlt_image_yuv444p10) { *format = mlt_image_yuv444p10; - error = convert_on_cpu(frame, image, format, output_format); + error = 1; // incomplete, defer to another converter } break; @@ -508,7 +483,7 @@ static int movit_render(EffectChain *chain, error = glsl->render_frame_rgba(chain, frame, width, height, image); if (!error && output_format != mlt_image_rgba) { *format = mlt_image_rgba; - error = convert_on_cpu(frame, image, format, output_format); + error = 1; // incomplete, defer to another converter } break; } @@ -625,15 +600,15 @@ static int convert_image(mlt_frame frame, mlt_image_format_name(output_format), mlt_frame_get_position(frame)); - // Use CPU if glsl not initialized or not supported. + // Check if OpenGL is not initialized or not supported. GlslManager *glsl = GlslManager::get_instance(); if (!glsl || !glsl->get_int("glsl_supported")) - return convert_on_cpu(frame, image, format, output_format); + return 1; - // Do non-GL image conversions on a CPU-based image converter. + // We only support movit and OpenGL formats if (*format != mlt_image_movit && output_format != mlt_image_movit && output_format != mlt_image_opengl_texture) - return convert_on_cpu(frame, image, format, output_format); + return 1; int error = 0; int width = mlt_properties_get_int(properties, "width"); @@ -650,8 +625,13 @@ static int convert_image(mlt_frame frame, // sent into the chain. if (output_format == mlt_image_movit) { if (*format != mlt_image_rgba && *format != mlt_image_rgba64 && mlt_frame_get_alpha(frame)) { - if (!convert_on_cpu(frame, image, format, mlt_image_rgba)) { - *format = mlt_image_rgba; + mlt_log_verbose( + NULL, + "filter movit.convert: frame has alpha but format is %s, converting to RGBA\n", + mlt_image_format_name(*format)); + if (mlt_frame_next_convert_image(frame, image, format, mlt_image_rgba)) { + mlt_log_error(NULL, "filter movit.convert: failed to convert frame to RGBA\n"); + return 1; } } @@ -687,14 +667,14 @@ static int convert_image(mlt_frame frame, if (leaf_service == (mlt_service) -1) { // Something on the way requested conversion to mlt_glsl, // but never added an effect. Don't build a Movit chain; - // just do the conversion and we're done. + // yield the conversion. mlt_producer producer = mlt_producer_cut_parent(mlt_frame_get_original_producer(frame)); MltInput *input = GlslManager::get_input(producer, frame); *image = GlslManager::get_input_pixel_pointer(producer, frame); *format = input->get_format(); delete input; GlslManager::get_instance()->unlock_service(frame); - return convert_on_cpu(frame, image, format, output_format); + return 1; } // Construct the chain unless we already have a good one. @@ -779,8 +759,10 @@ static int convert_image(mlt_frame frame, GlslManager::get_instance()->unlock_service(frame); - mlt_properties_set_int(properties, "format", output_format); - *format = output_format; + if (!error) { + mlt_properties_set_int(properties, "format", output_format); + *format = output_format; + } return error; } @@ -796,37 +778,11 @@ static mlt_frame process(mlt_filter filter, mlt_frame frame) "colorspace", mlt_service_profile(MLT_FILTER_SERVICE(filter))->colorspace); - frame->convert_image = convert_image; - - mlt_filter cpu_csc = (mlt_filter) mlt_properties_get_data(MLT_FILTER_PROPERTIES(filter), - "cpu_convert", - NULL); - mlt_properties_inc_ref(MLT_FILTER_PROPERTIES(cpu_csc)); - mlt_properties_set_data(properties, - "_movit cpu_convert", - cpu_csc, - 0, - (mlt_destructor) mlt_filter_close, - NULL); + mlt_frame_push_convert_image(frame, convert_image); return frame; } -static mlt_filter create_filter(mlt_profile profile, const char *effect) -{ - mlt_filter filter; - char *id = strdup(effect); - char *arg = strchr(id, ':'); - if (arg != NULL) - *arg++ = '\0'; - - filter = mlt_factory_filter(profile, id, arg); - if (filter) - mlt_properties_set_int(MLT_FILTER_PROPERTIES(filter), "_loader", 1); - free(id); - return filter; -} - extern "C" { mlt_filter filter_movit_convert_init(mlt_profile profile, @@ -840,16 +796,6 @@ mlt_filter filter_movit_convert_init(mlt_profile profile, if (glsl && (filter = mlt_filter_new())) { mlt_properties properties = MLT_FILTER_PROPERTIES(filter); glsl->add_ref(properties); - mlt_filter cpu_csc = create_filter(profile, "avcolor_space"); - if (!cpu_csc) - cpu_csc = create_filter(profile, "imageconvert"); - if (cpu_csc) - mlt_properties_set_data(MLT_FILTER_PROPERTIES(filter), - "cpu_convert", - cpu_csc, - 0, - (mlt_destructor) mlt_filter_close, - NULL); filter->process = process; } return filter; diff --git a/src/modules/movit/filter_movit_convert.yml b/src/modules/movit/filter_movit_convert.yml index ece4e1e772..4c2b293798 100644 --- a/src/modules/movit/filter_movit_convert.yml +++ b/src/modules/movit/filter_movit_convert.yml @@ -14,4 +14,4 @@ tags: description: Converts the colorspace and pixel format. notes: > This is not intended to be created directly. Rather, the loader producer - loads it if it is available to set the convert_image function pointer on frames. + loads it if it is available to register an image conversion callback on frames. diff --git a/src/modules/movit/filter_movit_crop.cpp b/src/modules/movit/filter_movit_crop.cpp index c5560af642..cdd7068217 100644 --- a/src/modules/movit/filter_movit_crop.cpp +++ b/src/modules/movit/filter_movit_crop.cpp @@ -77,10 +77,10 @@ static int get_image(mlt_frame frame, if (requested_format == mlt_image_none) return error; - if (!error && *format != mlt_image_movit && frame->convert_image) { + if (!error && *format != mlt_image_movit && mlt_frame_has_convert_image(frame)) { // Pin the requested format to the first one returned. // mlt_properties_set_int( MLT_PRODUCER_PROPERTIES(producer), "_movit image_format", *format ); - error = frame->convert_image(frame, image, format, mlt_image_movit); + error = mlt_frame_convert_image(frame, image, format, mlt_image_movit); } if (!error) { double left = mlt_properties_get_double(properties, "crop.left"); diff --git a/src/modules/openfx/filter_openfx.c b/src/modules/openfx/filter_openfx.c index fda54adb0f..8a69fe47bc 100644 --- a/src/modules/openfx/filter_openfx.c +++ b/src/modules/openfx/filter_openfx.c @@ -261,7 +261,7 @@ static int filter_get_image(mlt_frame frame, } if (*format != requested_format) { - frame->convert_image(frame, image, format, mlt_image_rgba); + mlt_frame_convert_image(frame, image, format, mlt_image_rgba); *format = mlt_image_rgba; } diff --git a/src/modules/qt/transition_qtblend.cpp b/src/modules/qt/transition_qtblend.cpp index a7c8f30ae7..b2b48f8efd 100644 --- a/src/modules/qt/transition_qtblend.cpp +++ b/src/modules/qt/transition_qtblend.cpp @@ -1,7 +1,7 @@ /* * transition_qtblend.cpp -- Qt composite transition * Copyright (c) 2016-2025 Jean-Baptiste Mardelle - * Copyright (c) 2025 Meltytech, LLC + * Copyright (c) 2025-2026 Meltytech, LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -217,10 +217,11 @@ static int get_image(mlt_frame a_frame, "progressive,distort,colorspace,full_range,force_full_luma," "top_field_first,color_trc"); // Prepare output image - if (b_frame->convert_image && (b_width != request_width || b_height != request_height)) { + if (mlt_frame_has_convert_image(b_frame) + && (b_width != request_width || b_height != request_height)) { mlt_properties_set_int(b_properties, "convert_image_width", request_width); mlt_properties_set_int(b_properties, "convert_image_height", request_height); - b_frame->convert_image(b_frame, &b_image, format, *format); + mlt_frame_convert_image(b_frame, &b_image, format, *format); *width = request_width; *height = request_height; } else { @@ -237,8 +238,8 @@ static int get_image(mlt_frame a_frame, error = mlt_frame_get_image(b_frame, &b_image, format, &b_width, &b_height, 0); } - if (b_frame->convert_image && !format_is_rgba(*format)) { - b_frame->convert_image(b_frame, &b_image, format, mlt_image_rgba); + if (mlt_frame_has_convert_image(b_frame) && !format_is_rgba(*format)) { + mlt_frame_convert_image(b_frame, &b_image, format, mlt_image_rgba); } if (*format != mlt_image_rgba64) *format = mlt_image_rgba; diff --git a/src/modules/xine/filter_deinterlace.c b/src/modules/xine/filter_deinterlace.c index 784df9134b..9c83f7586f 100644 --- a/src/modules/xine/filter_deinterlace.c +++ b/src/modules/xine/filter_deinterlace.c @@ -1,6 +1,6 @@ /* * filter_deinterlace.c -- deinterlace filter - * Copyright (C) 2003-2014 Meltytech, LLC + * Copyright (C) 2003-2026 Meltytech, LLC * * 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 @@ -141,8 +141,7 @@ static int deinterlace_yadif(mlt_frame frame, // Check that we aren't already progressive if (!error && previous_image && !progressive) { // OK, now we know we have work to do and can request the image in our format - frame->convert_image(previous_frame, &previous_image, format, mlt_image_yuv422); - + mlt_frame_convert_image(previous_frame, &previous_image, format, mlt_image_yuv422); mlt_service_unlock(MLT_FILTER_SERVICE(filter)); // Get the current frame's image @@ -351,9 +350,8 @@ static int filter_get_image(mlt_frame frame, if (!error && !progressive) { // OK, now we know we have work to do and can request the image in our format - error = frame->convert_image(frame, image, format, mlt_image_yuv422); + error = mlt_frame_convert_image(frame, image, format, mlt_image_yuv422); - // Check that we aren't already progressive if (!error && *image && *format == mlt_image_yuv422) { // Deinterlace the image using one of the Xine deinterlacers int image_size = mlt_image_format_size(*format, *width, *height, NULL); diff --git a/src/modules/xine/link_deinterlace.c b/src/modules/xine/link_deinterlace.c index 0ad7f28f02..2d370fa8ea 100644 --- a/src/modules/xine/link_deinterlace.c +++ b/src/modules/xine/link_deinterlace.c @@ -1,6 +1,6 @@ /* * link_deinterlace.c - * Copyright (C) 2023 Meltytech, LLC + * Copyright (C) 2023-2026 Meltytech, LLC * * 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 @@ -75,10 +75,10 @@ static int link_get_image(mlt_frame frame, if (srcimg.data) // Maybe already received during progressive check { if (srcimg.format != mlt_image_yuv422) { - error = frame->convert_image(frame, - (uint8_t **) &srcimg.data, - &srcimg.format, - mlt_image_yuv422); + error = mlt_frame_convert_image(frame, + (uint8_t **) &srcimg.data, + &srcimg.format, + mlt_image_yuv422); if (error) { mlt_log_error(MLT_LINK_SERVICE(self), "Failed to convert image\n"); return error; diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 8238855369..2ca7787aef 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -15,8 +15,9 @@ function(add_qt_test) target_link_libraries(${_testname} PRIVATE Qt${QT_MAJOR_VERSION}::Core Qt${QT_MAJOR_VERSION}::Test + mlt mlt++ - ${ARG_LINK_LIBRARIES} + ${arg_LINK_LIBRARIES} ) if(MSVC) target_link_libraries(${_testname} PRIVATE PThreads4W::PThreads4W) diff --git a/src/tests/test_frame/test_frame.cpp b/src/tests/test_frame/test_frame.cpp index 697561bd6d..39c3cd2e39 100644 --- a/src/tests/test_frame/test_frame.cpp +++ b/src/tests/test_frame/test_frame.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Meltytech, LLC + * Copyright (C) 2015-2026 Meltytech, LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -16,10 +16,42 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ +#include #include #include using namespace Mlt; +// --- helpers for convert_image tests --- + +static int g_convert_calls = 0; +static int g_convert_b_calls = 0; + +// A converter that always succeeds (returns 0). +static int convert_always(mlt_frame, uint8_t **, mlt_image_format *format, mlt_image_format output) +{ + ++g_convert_calls; + *format = output; + return 0; +} + +// A converter that always fails (returns 1), letting the next one try. +static int convert_fail(mlt_frame frame, + uint8_t **image, + mlt_image_format *format, + mlt_image_format output) +{ + ++g_convert_calls; + return mlt_frame_next_convert_image(frame, image, format, output); +} + +// A second succeed converter used to verify fallback ordering. +static int convert_b(mlt_frame, uint8_t **, mlt_image_format *format, mlt_image_format output) +{ + ++g_convert_b_calls; + *format = output; + return 0; +} + class TestFrame : public QObject { Q_OBJECT @@ -93,6 +125,106 @@ private Q_SLOTS: QCOMPARE(f1.ref_count(), 2); mlt_frame_close(frame); } + + // --- convert_image dispatch tests --- + + void ConvertImageShimSetOnInit() + { + // mlt_frame_init must install the shim so the field is never NULL. + mlt_frame frame = mlt_frame_init(NULL); + QVERIFY(frame->convert_image == mlt_frame_convert_image); + mlt_frame_close(frame); + } + + void HasConvertImageFalseWhenEmpty() + { + mlt_frame frame = mlt_frame_init(NULL); + QCOMPARE(mlt_frame_has_convert_image(frame), 0); + mlt_frame_close(frame); + } + + void HasConvertImageTrueAfterPush() + { + mlt_frame frame = mlt_frame_init(NULL); + mlt_frame_push_convert_image(frame, convert_always); + QCOMPARE(mlt_frame_has_convert_image(frame), 1); + mlt_frame_close(frame); + } + + void PushConvertImageDeduplicate() + { + mlt_frame frame = mlt_frame_init(NULL); + mlt_frame_push_convert_image(frame, convert_always); + mlt_frame_push_convert_image(frame, convert_always); // duplicate — must be ignored + // Invoke the dispatcher: a single converter should be called exactly once. + g_convert_calls = 0; + mlt_image_format fmt = mlt_image_rgb; + mlt_frame_convert_image(frame, NULL, &fmt, mlt_image_rgba); + QCOMPARE(g_convert_calls, 1); + mlt_frame_close(frame); + } + + void ConvertImageFirstSuccessWins() + { + // fail → b: only b should run and succeed; fail tries next via mlt_frame_next_convert_image. + mlt_frame frame = mlt_frame_init(NULL); + mlt_frame_push_convert_image(frame, convert_fail); + mlt_frame_push_convert_image(frame, convert_b); + g_convert_calls = 0; + g_convert_b_calls = 0; + mlt_image_format fmt = mlt_image_rgb; + int error = mlt_frame_convert_image(frame, NULL, &fmt, mlt_image_rgba); + QCOMPARE(error, 0); + QCOMPARE(g_convert_calls, 1); // convert_fail was called once + QCOMPARE(g_convert_b_calls, 1); // convert_b succeeded + QCOMPARE(fmt, mlt_image_rgba); + mlt_frame_close(frame); + } + + void ConvertImageAllFailReturnsError() + { + mlt_frame frame = mlt_frame_init(NULL); + mlt_frame_push_convert_image(frame, convert_fail); + g_convert_calls = 0; + mlt_image_format fmt = mlt_image_rgb; + int error = mlt_frame_convert_image(frame, NULL, &fmt, mlt_image_rgba); + QCOMPARE(error, 1); // no converter succeeded + mlt_frame_close(frame); + } + + void CopyConvertImagePropagatesToClone() + { + mlt_frame src = mlt_frame_init(NULL); + mlt_frame_push_convert_image(src, convert_always); + mlt_frame dst = mlt_frame_clone(src, 0); + QCOMPARE(mlt_frame_has_convert_image(dst), 1); + g_convert_calls = 0; + mlt_image_format fmt = mlt_image_rgb; + int error = mlt_frame_convert_image(dst, NULL, &fmt, mlt_image_rgba); + QCOMPARE(error, 0); + QCOMPARE(g_convert_calls, 1); + mlt_frame_close(dst); + mlt_frame_close(src); + } + + void CopyConvertImageDoesNotCopyTwice() + { + // mlt_frame_copy_convert_image() must not overwrite an existing list on dst. + mlt_frame src = mlt_frame_init(NULL); + mlt_frame_push_convert_image(src, convert_always); + mlt_frame dst = mlt_frame_init(NULL); + mlt_frame_push_convert_image(dst, convert_b); + mlt_frame_copy_convert_image(dst, src); + // dst already had a list — its own converter (convert_b) must still be there. + g_convert_calls = 0; + g_convert_b_calls = 0; + mlt_image_format fmt = mlt_image_rgb; + mlt_frame_convert_image(dst, NULL, &fmt, mlt_image_rgba); + QCOMPARE(g_convert_b_calls, 1); // dst's original converter ran + QCOMPARE(g_convert_calls, 0); // src's converter was NOT copied over + mlt_frame_close(dst); + mlt_frame_close(src); + } }; QTEST_APPLESS_MAIN(TestFrame) diff --git a/src/tests/test_mod_avformat/test_mod_avformat.cpp b/src/tests/test_mod_avformat/test_mod_avformat.cpp index d98111f785..fa41ef4749 100644 --- a/src/tests/test_mod_avformat/test_mod_avformat.cpp +++ b/src/tests/test_mod_avformat/test_mod_avformat.cpp @@ -1,5 +1,6 @@ /* * Copyright (C) 2026 Julius Künzel + * Copyright (C) 2026 Meltytech, LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -18,6 +19,7 @@ #include +#include #include using namespace Mlt; @@ -42,6 +44,51 @@ private Q_SLOTS: Profile profile; Producer producer(profile, "avformat-novalidate:blue.mpg"); } + + void LoaderAttachesImageConverters() + { + // The loader producer must attach at least one image converter (avcolor_space or + // imageconvert) via loader.ini image_convert so that frames produced through it + // have mlt_frame_has_convert_image() == true. + Profile profile; + // Use loader explicitly wrapping the color producer so no file system access is needed. + mlt_producer raw = mlt_factory_producer(profile.get_profile(), "loader", "color:black"); + QVERIFY(raw != NULL); + + mlt_frame frame = NULL; + mlt_service_get_frame(MLT_PRODUCER_SERVICE(raw), &frame, 0); + QVERIFY(frame != NULL); + + QVERIFY(mlt_frame_has_convert_image(frame)); + + mlt_frame_close(frame); + mlt_producer_close(raw); + } + + void LoaderNoGlSkipsMovitConverter() + { + // loader-nogl must not attach movit.convert but still attach a CPU converter. + Profile profile; + mlt_producer raw = mlt_factory_producer(profile.get_profile(), "loader-nogl", "color:black"); + QVERIFY(raw != NULL); + + mlt_frame frame = NULL; + mlt_service_get_frame(MLT_PRODUCER_SERVICE(raw), &frame, 0); + QVERIFY(frame != NULL); + QVERIFY(mlt_frame_has_convert_image(frame)); + + // Verify movit.convert is not among the attached filters. + mlt_service svc = MLT_PRODUCER_SERVICE(raw); + int count = mlt_service_filter_count(svc); + for (int i = 0; i < count; i++) { + mlt_filter f = mlt_service_filter(svc, i); + const char *id = mlt_properties_get(MLT_FILTER_PROPERTIES(f), "mlt_service"); + QVERIFY(qstrcmp(id, "movit.convert") != 0); + } + + mlt_frame_close(frame); + mlt_producer_close(raw); + } }; QTEST_APPLESS_MAIN(TestModAvformat) diff --git a/src/tests/test_tractor/test_tractor.cpp b/src/tests/test_tractor/test_tractor.cpp index 615e3e80f1..d53f4eda55 100644 --- a/src/tests/test_tractor/test_tractor.cpp +++ b/src/tests/test_tractor/test_tractor.cpp @@ -19,6 +19,7 @@ #include #include +#include #include using namespace Mlt; @@ -402,6 +403,37 @@ private Q_SLOTS: QCOMPARE(t.count(), 1); QCOMPARE(filter.get_track(), 0); } + + void ConvertImagePropagatesThroughMultitrack() + { + // The tractor's producer_get_image calls mlt_frame_copy_convert_image to + // propagate converters from each track frame onto the merged frame. + Tractor t(profile); + QVERIFY(t.is_valid()); + + // Wrap the track producer in loader so that image converter filters + // (avcolor_space, imageconvert) are attached and pushed onto track frames. + Producer p1(profile, "loader", "noise"); + QVERIFY(p1.is_valid()); + t.set_track(p1, 0); + + mlt_frame merged = NULL; + mlt_service_get_frame(MLT_PRODUCER_SERVICE(t.get_producer()), &merged, 0); + QVERIFY(merged != NULL); + + // Calling get_image triggers producer_get_image in the tractor, which pulls + // track frames through the loader filter chain (causing converters to be pushed + // onto them), then copies them onto the merged frame via + // mlt_frame_copy_convert_image. + uint8_t *image = NULL; + mlt_image_format fmt = mlt_image_rgb; + int width = 0, height = 0; + mlt_frame_get_image(merged, &image, &fmt, &width, &height, 0); + + QVERIFY(mlt_frame_has_convert_image(merged)); + + mlt_frame_close(merged); + } }; QTEST_APPLESS_MAIN(TestTractor) From 332faddd76fee6fa03f22d486fb1c1bcf2ab43e8 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Tue, 5 May 2026 17:28:30 -0700 Subject: [PATCH 2/8] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/framework/mlt.vers | 2 +- src/framework/mlt_tractor.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/framework/mlt.vers b/src/framework/mlt.vers index 222484533c..63622f72f0 100644 --- a/src/framework/mlt.vers +++ b/src/framework/mlt.vers @@ -695,4 +695,4 @@ MLT_7.40.0 { mlt_frame_convert_image; mlt_frame_next_convert_image; mlt_frame_copy_convert_image; -} MLT_7.34.0; +} MLT_7.36.0; diff --git a/src/framework/mlt_tractor.c b/src/framework/mlt_tractor.c index a8527ca3cf..21e3e9a7d7 100644 --- a/src/framework/mlt_tractor.c +++ b/src/framework/mlt_tractor.c @@ -537,10 +537,10 @@ static int producer_get_frame(mlt_producer parent, mlt_frame_ptr frame, int trac subtitle_properties); } - // Copy the format conversion callbacks + // Copy only audio conversion callbacks here. Image conversion callbacks + // must come from the track frame that is ultimately selected for video. if (!(*frame)->convert_audio && temp->convert_audio) (*frame)->convert_audio = temp->convert_audio; - mlt_frame_copy_convert_image(*frame, temp); // Check for last track done = mlt_properties_get_int(temp_properties, "last_track"); From bc676627338efbc553daf83ec3387f83cae198fe Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Tue, 5 May 2026 17:30:02 -0700 Subject: [PATCH 3/8] fix early returns not releasing lock --- src/modules/movit/filter_movit_convert.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/modules/movit/filter_movit_convert.cpp b/src/modules/movit/filter_movit_convert.cpp index d674961c85..a5d4c00b8a 100644 --- a/src/modules/movit/filter_movit_convert.cpp +++ b/src/modules/movit/filter_movit_convert.cpp @@ -631,6 +631,7 @@ static int convert_image(mlt_frame frame, mlt_image_format_name(*format)); if (mlt_frame_next_convert_image(frame, image, format, mlt_image_rgba)) { mlt_log_error(NULL, "filter movit.convert: failed to convert frame to RGBA\n"); + GlslManager::get_instance()->unlock_service(frame); return 1; } } @@ -642,6 +643,7 @@ static int convert_image(mlt_frame frame, if (!input) { mlt_log_error(nullptr, "filter movit.convert: create_input failed\n"); + GlslManager::get_instance()->unlock_service(frame); return 1; } @@ -651,6 +653,7 @@ static int convert_image(mlt_frame frame, if (!img_copy) { mlt_log_error(nullptr, "filter movit.convert: make_input_copy failed\n"); delete input; + GlslManager::get_instance()->unlock_service(frame); return 1; } @@ -721,6 +724,7 @@ static int convert_image(mlt_frame frame, if (!input) { delete chain; + GlslManager::get_instance()->unlock_service(frame); return 1; } @@ -744,6 +748,7 @@ static int convert_image(mlt_frame frame, uint8_t *planar = make_input_copy(*format, *image, width, height); if (!planar) { + GlslManager::get_instance()->unlock_service(frame); return 1; } From d26b3f375dc5c9f70ccc5466c24d69baa1dc423d Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Tue, 5 May 2026 17:41:17 -0700 Subject: [PATCH 4/8] change a log to debug level --- src/modules/movit/filter_movit_convert.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/movit/filter_movit_convert.cpp b/src/modules/movit/filter_movit_convert.cpp index a5d4c00b8a..0119e7986a 100644 --- a/src/modules/movit/filter_movit_convert.cpp +++ b/src/modules/movit/filter_movit_convert.cpp @@ -625,7 +625,7 @@ static int convert_image(mlt_frame frame, // sent into the chain. if (output_format == mlt_image_movit) { if (*format != mlt_image_rgba && *format != mlt_image_rgba64 && mlt_frame_get_alpha(frame)) { - mlt_log_verbose( + mlt_log_debug( NULL, "filter movit.convert: frame has alpha but format is %s, converting to RGBA\n", mlt_image_format_name(*format)); From 0a90633701af153fbc652b95f143c1cd3a22eee0 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Thu, 7 May 2026 13:58:38 -0700 Subject: [PATCH 5/8] add `mlt_frame_prepend_convert_image()` --- NEWS | 1 + src/framework/mlt.vers | 1 + src/framework/mlt_frame.c | 33 +++++++++++++++++++++++++++++++++ src/framework/mlt_frame.h | 1 + 4 files changed, 36 insertions(+) diff --git a/NEWS b/NEWS index b100ad20c1..56e6c1cc24 100644 --- a/NEWS +++ b/NEWS @@ -8,6 +8,7 @@ Framework Use `mlt_frame_push_convert_image()` to register a converter instead of setting `frame->convert_image` directly. Multiple converters are tried in registration order; the first that succeeds wins. New public API: + - `mlt_frame_prepend_convert_image()` - `mlt_frame_push_convert_image()` - `mlt_frame_convert_image()` - `mlt_frame_next_convert_image()` diff --git a/src/framework/mlt.vers b/src/framework/mlt.vers index 63622f72f0..23c2f012e0 100644 --- a/src/framework/mlt.vers +++ b/src/framework/mlt.vers @@ -691,6 +691,7 @@ MLT_7.36.0 { MLT_7.40.0 { global: mlt_frame_push_convert_image; + mlt_frame_prepend_convert_image; mlt_frame_has_convert_image; mlt_frame_convert_image; mlt_frame_next_convert_image; diff --git a/src/framework/mlt_frame.c b/src/framework/mlt_frame.c index 5a5536cf49..a200db8a61 100644 --- a/src/framework/mlt_frame.c +++ b/src/framework/mlt_frame.c @@ -889,6 +889,39 @@ void mlt_frame_push_convert_image(mlt_frame self, mlt_convert_image convert) mlt_deque_push_back(list, (void *) convert); } +/** Register an image-conversion callback on the frame. + * + * Callbacks are prepended to a list and dispatched in order by + * \p mlt_frame_convert_image. If \p convert is already registered on this + * frame it is silently ignored (deduplication). Has no effect if either + * argument is NULL. + * + * \public \memberof mlt_frame_s + * \param self a frame + * \param convert the conversion callback to register + */ +void mlt_frame_prepend_convert_image(mlt_frame self, mlt_convert_image convert) +{ + if (!self || !convert) + return; + mlt_properties props = MLT_FRAME_PROPERTIES(self); + mlt_deque list = mlt_properties_get_data(props, CONVERT_IMAGE_CALLBACKS, NULL); + if (!list) { + list = mlt_deque_init(); + mlt_properties_set_data(props, + CONVERT_IMAGE_CALLBACKS, + list, + 0, + (mlt_destructor) mlt_deque_close, + NULL); + } + // Deduplicate: don't register the same function pointer twice. + for (int i = 0; i < mlt_deque_count(list); i++) + if (mlt_deque_peek(list, i) == (void *) convert) + return; + mlt_deque_push_front(list, (void *) convert); +} + /** Determine whether any image-conversion callbacks are registered. * * \public \memberof mlt_frame_s diff --git a/src/framework/mlt_frame.h b/src/framework/mlt_frame.h index 889b2130b8..a1fd10132a 100644 --- a/src/framework/mlt_frame.h +++ b/src/framework/mlt_frame.h @@ -184,6 +184,7 @@ MLT_EXPORT void mlt_frame_close(mlt_frame self); MLT_EXPORT mlt_properties mlt_frame_unique_properties(mlt_frame self, mlt_service service); MLT_EXPORT mlt_properties mlt_frame_get_unique_properties(mlt_frame self, mlt_service service); MLT_EXPORT void mlt_frame_push_convert_image(mlt_frame self, mlt_convert_image convert); +MLT_EXPORT void mlt_frame_prepend_convert_image(mlt_frame self, mlt_convert_image convert); MLT_EXPORT int mlt_frame_has_convert_image(mlt_frame self); MLT_EXPORT int mlt_frame_convert_image(mlt_frame self, uint8_t **image, From ba97d1e8f556446fbb983c73a375a98a3ee62790 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Wed, 6 May 2026 09:54:02 -0700 Subject: [PATCH 6/8] fix cppcheck and redundant workflow runs --- .github/workflows/build-distros.yml | 3 +++ .github/workflows/build-linux.yml | 10 +++++++++- .github/workflows/build-msys2-mingw64.yml | 11 ++++++++++- .github/workflows/build-windows-msvc.yml | 12 ++++++++++-- .github/workflows/static-code-analysis.yml | 10 +++++++++- makefile | 6 +++++- 6 files changed, 46 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-distros.yml b/.github/workflows/build-distros.yml index 167e759695..5d119b68df 100644 --- a/.github/workflows/build-distros.yml +++ b/.github/workflows/build-distros.yml @@ -20,6 +20,9 @@ on: - debian-stable - fedora-42 - fedora-38 +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: build: diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 0e618634f2..07e06aa3b9 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -1,6 +1,14 @@ name: build-test-linux-on-push -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: python: diff --git a/.github/workflows/build-msys2-mingw64.yml b/.github/workflows/build-msys2-mingw64.yml index 2417926cd0..b19d15b924 100644 --- a/.github/workflows/build-msys2-mingw64.yml +++ b/.github/workflows/build-msys2-mingw64.yml @@ -1,6 +1,15 @@ name: build-test-msys2-mingw64-on-push -on: [push, pull_request, workflow_dispatch] +on: + push: + branches: + - master + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: build: diff --git a/.github/workflows/build-windows-msvc.yml b/.github/workflows/build-windows-msvc.yml index ddc47c3d37..83edfa5237 100644 --- a/.github/workflows/build-windows-msvc.yml +++ b/.github/workflows/build-windows-msvc.yml @@ -1,6 +1,14 @@ name: build-test-windows-msvc-on-push -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true permissions: packages: write @@ -55,4 +63,4 @@ jobs: - name: Run tests run: | - ctest --test-dir build -C Debug + ctest --test-dir build -C Debug --rerun-failed --output-on-failure diff --git a/.github/workflows/static-code-analysis.yml b/.github/workflows/static-code-analysis.yml index 00a95de14e..03644d1a79 100644 --- a/.github/workflows/static-code-analysis.yml +++ b/.github/workflows/static-code-analysis.yml @@ -1,6 +1,14 @@ name: static-code-analysis -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: cppcheck: diff --git a/makefile b/makefile index d16b0a056c..5abbec01ae 100644 --- a/makefile +++ b/makefile @@ -29,4 +29,8 @@ cppcheck: --include=src/framework/mlt_types.h \ --library=cppcheck.cfg \ --suppress=ctuOneDefinitionRuleViolation \ - --suppress=syntaxError:src/modules/xml/common.c + --suppress=syntaxError:src/modules/xml/common.c \ + --suppress=syntaxError:src/modules/placebo/filter_placebo_convert.c \ + --suppress=syntaxError:src/modules/placebo/filter_placebo_render.c \ + --suppress=syntaxError:src/modules/placebo/filter_placebo_shader.c \ + --suppress=syntaxError:src/modules/placebo/gpu_context.c From abead7f012d01396b6042202678e7dc01078f120 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Wed, 6 May 2026 10:17:28 -0700 Subject: [PATCH 7/8] fix ctest on msvc --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index df7ca47930..679bf3a7ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,7 +98,7 @@ endif() if(NOT EXISTS ${MLT_DATA_OUTPUT_DIRECTORY}) if(WIN32) # symlinks require admin rights on Windows - file(COPY "${CMAKE_SOURCE_DIR}/src/modules" DESTINATION "${CMAKE_BINARY_DIR}/out/share" FILES_MATCHING REGEX yml|txt) + file(COPY "${CMAKE_SOURCE_DIR}/src/modules" DESTINATION "${CMAKE_BINARY_DIR}/out/share" FILES_MATCHING REGEX "yml|txt|ini|dict") file(RENAME "${CMAKE_BINARY_DIR}/out/share/modules" "${MLT_DATA_OUTPUT_DIRECTORY}") file(COPY "${CMAKE_SOURCE_DIR}/presets" DESTINATION "${MLT_DATA_OUTPUT_DIRECTORY}") file(COPY "${CMAKE_SOURCE_DIR}/profiles" DESTINATION "${MLT_DATA_OUTPUT_DIRECTORY}") From 1844703bd151fa54fec2198ebc0ca4d6a91611e2 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Thu, 7 May 2026 16:50:28 -0700 Subject: [PATCH 8/8] mlt_frame_append_convert_image() --- NEWS | 6 +-- src/framework/mlt.vers | 2 +- src/framework/mlt_frame.c | 13 +++++- src/framework/mlt_frame.h | 4 +- src/modules/avformat/filter_avcolour_space.c | 2 +- src/modules/core/filter_imageconvert.c | 2 +- src/modules/movit/filter_movit_convert.cpp | 2 +- src/tests/test_frame/test_frame.cpp | 43 +++++++++++++++----- 8 files changed, 53 insertions(+), 21 deletions(-) diff --git a/NEWS b/NEWS index 56e6c1cc24..81a405fe14 100644 --- a/NEWS +++ b/NEWS @@ -5,11 +5,11 @@ Version 7.40.0 Framework - Added list-based image-conversion callback dispatch on mlt_frame. - Use `mlt_frame_push_convert_image()` to register a converter instead of + Use `mlt_frame_append_convert_image()` to register a converter instead of setting `frame->convert_image` directly. Multiple converters are tried in registration order; the first that succeeds wins. New public API: - `mlt_frame_prepend_convert_image()` - - `mlt_frame_push_convert_image()` + - `mlt_frame_append_convert_image()` - `mlt_frame_convert_image()` - `mlt_frame_next_convert_image()` - `mlt_frame_has_convert_image()` @@ -19,7 +19,7 @@ Framework External code that set this field directly to a custom function will no longer have that function called. If you really need that (never heard of someone who does), clear the frame's "_convert_image_callbacks" property - and use `mlt_frame_push_convert_image()` instead. + and use `mlt_frame_append_convert_image()` instead. - Image converters (`movit.convert`, `avcolor_space`, `imageconvert`) are now attached data-driven via `loader.ini` key `image_convert`. diff --git a/src/framework/mlt.vers b/src/framework/mlt.vers index 23c2f012e0..52f49eb0c3 100644 --- a/src/framework/mlt.vers +++ b/src/framework/mlt.vers @@ -690,7 +690,7 @@ MLT_7.36.0 { MLT_7.40.0 { global: - mlt_frame_push_convert_image; + mlt_frame_append_convert_image; mlt_frame_prepend_convert_image; mlt_frame_has_convert_image; mlt_frame_convert_image; diff --git a/src/framework/mlt_frame.c b/src/framework/mlt_frame.c index a200db8a61..1b36877549 100644 --- a/src/framework/mlt_frame.c +++ b/src/framework/mlt_frame.c @@ -862,12 +862,16 @@ static int do_convert_image( * \p mlt_frame_convert_image. If \p convert is already registered on this * frame it is silently ignored (deduplication). Has no effect if either * argument is NULL. + * + * This function is primarily used by the \p loader producer per \p loader.ini + * to add the default and fallback converters. Most modules or external callers + * will want to use \p mlt_frame_prepend_convert_image. * * \public \memberof mlt_frame_s * \param self a frame * \param convert the conversion callback to register */ -void mlt_frame_push_convert_image(mlt_frame self, mlt_convert_image convert) +void mlt_frame_append_convert_image(mlt_frame self, mlt_convert_image convert) { if (!self || !convert) return; @@ -895,6 +899,13 @@ void mlt_frame_push_convert_image(mlt_frame self, mlt_convert_image convert) * \p mlt_frame_convert_image. If \p convert is already registered on this * frame it is silently ignored (deduplication). Has no effect if either * argument is NULL. + * + * This is more useful for a specialized or incomplete converter. Return + * error from your callback to hand-off to the next converters, one of which + * ought to be able to do the conversion. This is not all-or-nothing; you can + * still convert to a supported format that your code handles even if it does + * not match the requested format. Simply update the \p format argument and/or + * \p image argument and return a non-zero value. * * \public \memberof mlt_frame_s * \param self a frame diff --git a/src/framework/mlt_frame.h b/src/framework/mlt_frame.h index a1fd10132a..e6cb9d25fb 100644 --- a/src/framework/mlt_frame.h +++ b/src/framework/mlt_frame.h @@ -103,7 +103,7 @@ struct mlt_frame_s /** Image format conversion dispatcher (read-only after init). * Always set to mlt_frame_convert_image by mlt_frame_init(). - * Do not set this field directly; use mlt_frame_push_convert_image() instead. + * Do not set this field directly; use mlt_frame_prepend_convert_image() instead. * \param self a frame * \param[in,out] image a buffer of image data * \param[in,out] input the image format of supplied image data @@ -183,7 +183,7 @@ MLT_EXPORT mlt_producer mlt_frame_get_original_producer(mlt_frame self); MLT_EXPORT void mlt_frame_close(mlt_frame self); MLT_EXPORT mlt_properties mlt_frame_unique_properties(mlt_frame self, mlt_service service); MLT_EXPORT mlt_properties mlt_frame_get_unique_properties(mlt_frame self, mlt_service service); -MLT_EXPORT void mlt_frame_push_convert_image(mlt_frame self, mlt_convert_image convert); +MLT_EXPORT void mlt_frame_append_convert_image(mlt_frame self, mlt_convert_image convert); MLT_EXPORT void mlt_frame_prepend_convert_image(mlt_frame self, mlt_convert_image convert); MLT_EXPORT int mlt_frame_has_convert_image(mlt_frame self); MLT_EXPORT int mlt_frame_convert_image(mlt_frame self, diff --git a/src/modules/avformat/filter_avcolour_space.c b/src/modules/avformat/filter_avcolour_space.c index f04d7cf7c2..79950244e6 100644 --- a/src/modules/avformat/filter_avcolour_space.c +++ b/src/modules/avformat/filter_avcolour_space.c @@ -355,7 +355,7 @@ static int convert_image(mlt_frame frame, static mlt_frame filter_process(mlt_filter filter, mlt_frame frame) { - mlt_frame_push_convert_image(frame, convert_image); + mlt_frame_append_convert_image(frame, convert_image); return frame; } diff --git a/src/modules/core/filter_imageconvert.c b/src/modules/core/filter_imageconvert.c index 4f5ed5d283..6506cfdd97 100644 --- a/src/modules/core/filter_imageconvert.c +++ b/src/modules/core/filter_imageconvert.c @@ -493,7 +493,7 @@ static int convert_image(mlt_frame frame, static mlt_frame filter_process(mlt_filter filter, mlt_frame frame) { - mlt_frame_push_convert_image(frame, convert_image); + mlt_frame_append_convert_image(frame, convert_image); return frame; } diff --git a/src/modules/movit/filter_movit_convert.cpp b/src/modules/movit/filter_movit_convert.cpp index 0119e7986a..d8bec38205 100644 --- a/src/modules/movit/filter_movit_convert.cpp +++ b/src/modules/movit/filter_movit_convert.cpp @@ -783,7 +783,7 @@ static mlt_frame process(mlt_filter filter, mlt_frame frame) "colorspace", mlt_service_profile(MLT_FILTER_SERVICE(filter))->colorspace); - mlt_frame_push_convert_image(frame, convert_image); + mlt_frame_append_convert_image(frame, convert_image); return frame; } diff --git a/src/tests/test_frame/test_frame.cpp b/src/tests/test_frame/test_frame.cpp index 39c3cd2e39..5504b842b2 100644 --- a/src/tests/test_frame/test_frame.cpp +++ b/src/tests/test_frame/test_frame.cpp @@ -143,19 +143,40 @@ private Q_SLOTS: mlt_frame_close(frame); } - void HasConvertImageTrueAfterPush() + void HasConvertImageTrueAfterAppend() { mlt_frame frame = mlt_frame_init(NULL); - mlt_frame_push_convert_image(frame, convert_always); + mlt_frame_append_convert_image(frame, convert_always); QCOMPARE(mlt_frame_has_convert_image(frame), 1); mlt_frame_close(frame); } - void PushConvertImageDeduplicate() + void AppendConvertImageDeduplicate() { mlt_frame frame = mlt_frame_init(NULL); - mlt_frame_push_convert_image(frame, convert_always); - mlt_frame_push_convert_image(frame, convert_always); // duplicate — must be ignored + mlt_frame_append_convert_image(frame, convert_always); + mlt_frame_append_convert_image(frame, convert_always); // duplicate — must be ignored + // Invoke the dispatcher: a single converter should be called exactly once. + g_convert_calls = 0; + mlt_image_format fmt = mlt_image_rgb; + mlt_frame_convert_image(frame, NULL, &fmt, mlt_image_rgba); + QCOMPARE(g_convert_calls, 1); + mlt_frame_close(frame); + } + + void HasConvertImageTrueAfterPrepend() + { + mlt_frame frame = mlt_frame_init(NULL); + mlt_frame_prepend_convert_image(frame, convert_always); + QCOMPARE(mlt_frame_has_convert_image(frame), 1); + mlt_frame_close(frame); + } + + void PrependConvertImageDeduplicate() + { + mlt_frame frame = mlt_frame_init(NULL); + mlt_frame_prepend_convert_image(frame, convert_always); + mlt_frame_prepend_convert_image(frame, convert_always); // duplicate — must be ignored // Invoke the dispatcher: a single converter should be called exactly once. g_convert_calls = 0; mlt_image_format fmt = mlt_image_rgb; @@ -168,8 +189,8 @@ private Q_SLOTS: { // fail → b: only b should run and succeed; fail tries next via mlt_frame_next_convert_image. mlt_frame frame = mlt_frame_init(NULL); - mlt_frame_push_convert_image(frame, convert_fail); - mlt_frame_push_convert_image(frame, convert_b); + mlt_frame_append_convert_image(frame, convert_fail); + mlt_frame_append_convert_image(frame, convert_b); g_convert_calls = 0; g_convert_b_calls = 0; mlt_image_format fmt = mlt_image_rgb; @@ -184,7 +205,7 @@ private Q_SLOTS: void ConvertImageAllFailReturnsError() { mlt_frame frame = mlt_frame_init(NULL); - mlt_frame_push_convert_image(frame, convert_fail); + mlt_frame_append_convert_image(frame, convert_fail); g_convert_calls = 0; mlt_image_format fmt = mlt_image_rgb; int error = mlt_frame_convert_image(frame, NULL, &fmt, mlt_image_rgba); @@ -195,7 +216,7 @@ private Q_SLOTS: void CopyConvertImagePropagatesToClone() { mlt_frame src = mlt_frame_init(NULL); - mlt_frame_push_convert_image(src, convert_always); + mlt_frame_append_convert_image(src, convert_always); mlt_frame dst = mlt_frame_clone(src, 0); QCOMPARE(mlt_frame_has_convert_image(dst), 1); g_convert_calls = 0; @@ -211,9 +232,9 @@ private Q_SLOTS: { // mlt_frame_copy_convert_image() must not overwrite an existing list on dst. mlt_frame src = mlt_frame_init(NULL); - mlt_frame_push_convert_image(src, convert_always); + mlt_frame_append_convert_image(src, convert_always); mlt_frame dst = mlt_frame_init(NULL); - mlt_frame_push_convert_image(dst, convert_b); + mlt_frame_append_convert_image(dst, convert_b); mlt_frame_copy_convert_image(dst, src); // dst already had a list — its own converter (convert_b) must still be there. g_convert_calls = 0;