From 0d287afad68dea4ff21e05fd2adf6c918195352f Mon Sep 17 00:00:00 2001 From: Vinicius Deolindo Date: Tue, 13 Oct 2026 00:16:44 -0300 Subject: [PATCH 1/8] luma: don't convert opaque frames to rgba for fix_background_alpha Previously, a dissolve between two opaque 4K60 H.264 clips at 100 Mbit/s ran at 8.3 fps on a 16-core desktop, when played on one thread as the Kdenlive monitor does. Most of this time went to color conversions that changed nothing. Kdenlive sets fix_background_alpha on every dissolve, and with this property, the luma transition requested both frames as rgba and converted the result back to yuv422. It did this only to make the padding that filter_resize adds transparent, so for opaque video, the four conversions had no effect. The round trip also slowed down the work after the transition, because it left the result with an alpha mask that was fully opaque. Kdenlive composites its tracks with a qtblend transition, which saw this mask and so composited the frame in rgba too. The transition now requests yuv422 and marks both frames with fix_background_alpha. The padding stays transparent, because when filter_resize pads a marked frame that has no alpha mask, it first adds an opaque one. The alpha of the result also stays correct: when one frame is translucent, the transition gives the other frame an opaque mask if it has none, as the round trip did, so that alpha_over can still compute it. With this change, the same dissolve runs at 73 fps. Its luma is also closer to the source, because the rgba round trip clipped colors that rgb cannot represent. In the first frame of the dissolve, luma is now identical to the decoded source, and before, the two differed by a PSNR of 38 dB. Apart from this, the output does not change. These cases give the same result as before: - A 16:9 clip in a 4:3 profile gets transparent padding. - With alpha_over, a dissolve into a clip with transparency shows the track below through it, with the same colors in every frame, also with a luma map and with invert. - With fix_background_alpha off, the output is bit-identical. --- src/modules/core/filter_resize.c | 11 ++++++ src/modules/core/transition_luma.c | 63 +++++++++++++++++++++--------- 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/src/modules/core/filter_resize.c b/src/modules/core/filter_resize.c index 1fffebd8a..cc04ac947 100644 --- a/src/modules/core/filter_resize.c +++ b/src/modules/core/filter_resize.c @@ -176,6 +176,17 @@ static uint8_t *frame_resize_image(mlt_frame frame, int owidth, int oheight, mlt // Now update the frame mlt_frame_set_image(frame, output, owidth * (oheight + 1) * bpp, mlt_pool_release); + // An opaque image without an alpha mask gets one, so that the padding can be + // transparent. This gives the same result as padding an rgba image, without + // converting the image to rgba and back. + if (format == mlt_image_yuv422 && !alpha + && mlt_properties_get_int(properties, "fix_background_alpha")) { + alpha_size = iwidth * iheight; + alpha = mlt_pool_alloc(alpha_size); + memset(alpha, 255, alpha_size); + mlt_frame_set_alpha(frame, alpha, alpha_size, mlt_pool_release); + } + // We should resize the alpha too if (format != mlt_image_rgba && alpha && alpha_size >= iwidth * iheight) { alpha = resize_alpha(alpha, owidth, oheight, iwidth, iheight, alpha_value); diff --git a/src/modules/core/transition_luma.c b/src/modules/core/transition_luma.c index c8eb438be..4bd9d4ae8 100644 --- a/src/modules/core/transition_luma.c +++ b/src/modules/core/transition_luma.c @@ -128,6 +128,16 @@ static int dissolve_slice_yuv422(int id, int index, int count, void *context) return 0; } +// Gives a frame an opaque alpha mask, as converting it to rgba and back does. +static uint8_t *add_opaque_alpha(mlt_frame frame, int width, int height) +{ + int size = width * height; + uint8_t *alpha = mlt_pool_alloc(size); + memset(alpha, 255, size); + mlt_frame_set_alpha(frame, alpha, size, mlt_pool_release); + return alpha; +} + static inline int dissolve_yuv422(mlt_frame frame, mlt_frame that, float weight, @@ -140,27 +150,35 @@ 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 && mlt_frame_has_convert_image(frame)) - ? mlt_image_rgba - : mlt_image_yuv422; + mlt_image_format format = mlt_image_yuv422; uint8_t *p_src, *p_dest; uint8_t *alpha_src; uint8_t *alpha_dst; int mix = weight * (1 << 16); + if (fix_background_alpha) { + // Tell filter_resize to make the padding transparent (see filter_resize.c). + mlt_properties_set_int(MLT_FRAME_PROPERTIES(frame), "fix_background_alpha", 1); + mlt_properties_set_int(MLT_FRAME_PROPERTIES(that), "fix_background_alpha", 1); + } + mlt_frame_get_image(frame, &p_dest, &format, &width, &height, 1); - 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 && mlt_frame_has_convert_image(that)) - format = mlt_image_rgba; + format = mlt_image_yuv422; mlt_frame_get_image(that, &p_src, &format, &width_src, &height_src, 0); - 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)); + // An opaque frame without an alpha mask must get one when the other frame is + // translucent, so that the alpha of the result is computed (see alpha_over). + if (fix_background_alpha && is_translucent) { + if (!alpha_dst) + alpha_dst = add_opaque_alpha(frame, width, height); + if (!alpha_src) + alpha_src = add_opaque_alpha(that, width_src, height_src); + } + // Pick the lesser of two evils ;-) width_src = width_src > width ? width : width_src; height_src = height_src > height ? height : height_src; @@ -341,29 +359,27 @@ 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 && mlt_frame_has_convert_image(a_frame)) - ? mlt_image_rgba - : mlt_image_yuv422; - mlt_image_format format_dest = (fix_background_alpha && mlt_frame_has_convert_image(b_frame)) - ? mlt_image_rgba - : mlt_image_yuv422; + mlt_image_format format_src = mlt_image_yuv422; + mlt_image_format format_dest = mlt_image_yuv422; uint8_t *p_src, *p_dest; uint8_t *alpha_src, *alpha_dest; int i, j; int stride_src; int stride_dest; + if (fix_background_alpha) { + // Tell filter_resize to make the padding transparent (see filter_resize.c). + mlt_properties_set_int(MLT_FRAME_PROPERTIES(a_frame), "fix_background_alpha", 1); + mlt_properties_set_int(MLT_FRAME_PROPERTIES(b_frame), "fix_background_alpha", 1); + } + if (mlt_properties_get(&a_frame->parent, "distort")) mlt_properties_set(&b_frame->parent, "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 && 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 && 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) @@ -372,6 +388,15 @@ static void luma_composite_yuv422(mlt_frame a_frame, int is_translucent = (alpha_dest && !is_opaque(alpha_dest, width_dest, height_dest)) || (alpha_src && !is_opaque(alpha_src, width_src, height_src)); + // An opaque frame without an alpha mask must get one when the other frame is + // translucent, so that the alpha of the result is computed (see alpha_over). + if (fix_background_alpha && is_translucent) { + if (!alpha_dest) + alpha_dest = add_opaque_alpha(a_frame, width_dest, height_dest); + if (!alpha_src) + alpha_src = add_opaque_alpha(b_frame, width_src, height_src); + } + // Pick the lesser of two evils ;-) width_src = width_src > width_dest ? width_dest : width_src; height_src = height_src > height_dest ? height_dest : height_src; From 992b829a876113e115a70df4dea77df41516eedc Mon Sep 17 00:00:00 2001 From: Vinicius Deolindo Date: Tue, 13 Oct 2026 00:16:56 -0300 Subject: [PATCH 2/8] luma: dissolve opaque yuv422 frames in slices Previously, the luma transition split the dissolve of translucent frames into slices, but it mixed opaque frames line by line on the calling thread. For 4K frames, this loop took a large part of the time of each frame. Opaque frames now use slices too, as translucent frames do. The output does not change, and on a 16-core desktop, a dissolve between two 4K60 H.264 clips at 100 Mbit/s goes from 73 to 77 fps. --- src/modules/core/transition_luma.c | 63 +++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/src/modules/core/transition_luma.c b/src/modules/core/transition_luma.c index 4bd9d4ae8..397eb2607 100644 --- a/src/modules/core/transition_luma.c +++ b/src/modules/core/transition_luma.c @@ -138,6 +138,50 @@ static uint8_t *add_opaque_alpha(mlt_frame frame, int width, int height) return alpha; } +struct dissolve_opaque_context +{ + uint8_t *dst_image; + uint8_t *src_image; + uint8_t *dst_alpha; + uint8_t *src_alpha; + int dst_width; + int src_width; + int height; + int mix; +}; + +static int dissolve_opaque_slice_yuv422(int id, int index, int count, void *context) +{ + struct dissolve_opaque_context ctx = *((struct dissolve_opaque_context *) context); + int slice_start, slice_height = mlt_slices_size_slice(count, index, ctx.height, &slice_start); + + ctx.dst_image += slice_start * (ctx.dst_width << 1); + ctx.src_image += slice_start * (ctx.src_width << 1); + if (ctx.dst_alpha) + ctx.dst_alpha += slice_start * ctx.dst_width; + if (ctx.src_alpha) + ctx.src_alpha += slice_start * ctx.src_width; + + for (int i = 0; i < slice_height; i++) { + composite_line_yuv(ctx.dst_image, + ctx.src_image, + ctx.src_width, + ctx.src_alpha, + ctx.dst_alpha, + ctx.mix, + NULL, + 0, + 0); + ctx.dst_image += ctx.dst_width << 1; + ctx.src_image += ctx.src_width << 1; + if (ctx.dst_alpha) + ctx.dst_alpha += ctx.dst_width; + if (ctx.src_alpha) + ctx.src_alpha += ctx.src_width; + } + return 0; +} + static inline int dissolve_yuv422(mlt_frame frame, mlt_frame that, float weight, @@ -148,7 +192,6 @@ static inline int dissolve_yuv422(mlt_frame frame, int fix_background_alpha) { int ret = 0; - int i = height + 1; int width_src = width, height_src = height; mlt_image_format format = mlt_image_yuv422; uint8_t *p_src, *p_dest; @@ -193,15 +236,15 @@ static inline int dissolve_yuv422(mlt_frame frame, .weight = weight}; mlt_slices_run_normal(threads, dissolve_slice_yuv422, &context); } else { - while (--i) { - composite_line_yuv(p_dest, p_src, width_src, alpha_src, alpha_dst, mix, NULL, 0, 0); - p_src += width_src << 1; - p_dest += width << 1; - if (alpha_src) - alpha_src += width_src; - if (alpha_dst) - alpha_dst += width; - } + struct dissolve_opaque_context context = {.dst_image = p_dest, + .src_image = p_src, + .dst_alpha = alpha_dst, + .src_alpha = alpha_src, + .dst_width = width, + .src_width = width_src, + .height = height, + .mix = mix}; + mlt_slices_run_normal(threads, dissolve_opaque_slice_yuv422, &context); } return ret; From 688baae60ea1154ee6ffc8b884aab3988b9078fe Mon Sep 17 00:00:00 2001 From: Vinicius Deolindo Date: Tue, 13 Oct 2026 02:16:50 -0300 Subject: [PATCH 3/8] avformat: find the error concealment frame in the image cache Previously, during forward playback, the producer made a deep copy of each decoded frame in last_good_frame. It needs the last good frame to conceal decoding errors: when a frame fails to decode, the producer shows the last good frame in its place. But the image cache already held a copy of the same frame, so for a 4K clip, each frame cost a second 16 MB copy that was useful only after an error. When the image cache is on, the producer now keeps only the position of the last good frame, and gets the frame from the cache when decoding fails. The cache can lose that frame in two cases, so the producer copies the frame before they happen: - when frames before it go into the cache, which happens during backward playback or after a seek back; - when the cache is reset. Without the image cache, the producer still copies every frame. The producer conceals errors with the same frames as before. This was checked with clips with corrupted parts, played forward, played backward, after a seek back, after a cache reset, and with a small image cache or none. With this change, forward playback is faster. On a 16-core desktop, a dissolve between two 4K60 H.264 clips at 100 Mbit/s goes from 61 to 69 fps, and in a Kdenlive project with such clips, from 73 to 86 fps. --- src/modules/avformat/producer_avformat.c | 66 ++++++++++++++++-------- 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/src/modules/avformat/producer_avformat.c b/src/modules/avformat/producer_avformat.c index f1cc5209c..389f78366 100644 --- a/src/modules/avformat/producer_avformat.c +++ b/src/modules/avformat/producer_avformat.c @@ -131,6 +131,8 @@ struct producer_avformat_s AVRational video_time_base; mlt_frame last_good_frame; // for video error concealment int last_good_position; // for video error concealment + // Image cache key of the last good frame when last_good_frame is NULL. + mlt_position last_good_cache_position; AVFilterGraph *vfilter_graph; AVFilterContext *vfilter_in; AVFilterContext *vfilter_out; @@ -230,6 +232,7 @@ mlt_producer producer_avformat_init(mlt_profile profile, const char *service, ch if (file) { // Construct the producer producer_avformat self = calloc(1, sizeof(struct producer_avformat_s)); + self->last_good_cache_position = POSITION_INVALID; producer = calloc(1, sizeof(struct mlt_producer_s)); // Initialise it @@ -2755,6 +2758,15 @@ static void init_cache(mlt_properties properties, mlt_cache *cache) mlt_cache_set_size(*cache, cache_size); } +// Copy the last good frame out of the image cache before the cache can lose it. +static void keep_last_good_frame(producer_avformat self) +{ + if (!self->last_good_frame && self->image_cache + && self->last_good_cache_position != POSITION_INVALID) + self->last_good_frame = mlt_cache_get_frame(self->image_cache, + self->last_good_cache_position); +} + /** Get an image from a frame. */ @@ -2801,6 +2813,7 @@ static int producer_get_image(mlt_frame frame, if (self->reset_image_cache) { self->reset_image_cache = 0; + keep_last_good_frame(self); mlt_cache_close(self->image_cache); self->image_cache = NULL; av_frame_free(&self->video_frame); @@ -3240,6 +3253,9 @@ static int producer_get_image(mlt_frame frame, if (image_size > 0) { mlt_properties_set_int(frame_properties, "format", *format); + // An earlier frame does not become the last good one, but caching it may evict that. + if (self->current_position < self->last_good_position) + keep_last_good_frame(self); // Cache the image for rapid repeated access. if (self->image_cache) { if (is_album_art(self)) { @@ -3251,33 +3267,40 @@ static int producer_get_image(mlt_frame frame, mlt_cache_put_frame_image(self->image_cache, frame); } } - // Clone frame for error concealment. + // For error concealment; avoid a second copy when the image cache already has one. if (self->current_position >= self->last_good_position) { self->last_good_position = self->current_position; if (self->last_good_frame) mlt_frame_close(self->last_good_frame); - self->last_good_frame = mlt_frame_clone(frame, 1); + self->last_good_frame = self->image_cache ? NULL : mlt_frame_clone(frame, 1); + self->last_good_cache_position = self->image_cache ? position : POSITION_INVALID; } - } else if (self->last_good_frame) { + } else { // Use last known good frame if there was a decoding failure. - mlt_frame original = mlt_frame_clone(self->last_good_frame, 1); - mlt_properties orig_props = MLT_FRAME_PROPERTIES(original); - int size = 0; - - *buffer = mlt_frame_get_alpha_size(original, &size); - if (*buffer) - mlt_frame_set_alpha(frame, *buffer, size, NULL); - *buffer = mlt_properties_get_data(orig_props, "image", &size); - mlt_frame_set_image(frame, *buffer, size, NULL); - mlt_properties_set_data(frame_properties, - "avformat.conceal_error", - original, - 0, - (mlt_destructor) mlt_frame_close, - NULL); - *format = mlt_properties_get_int(orig_props, "format"); - set_image_size(self, width, height); - got_picture = 1; + mlt_frame original = NULL; + if (self->last_good_frame) + original = mlt_frame_clone(self->last_good_frame, 1); + else if (self->image_cache && self->last_good_cache_position != POSITION_INVALID) + original = mlt_cache_get_frame(self->image_cache, self->last_good_cache_position); + if (original) { + mlt_properties orig_props = MLT_FRAME_PROPERTIES(original); + int size = 0; + + *buffer = mlt_frame_get_alpha_size(original, &size); + if (*buffer) + mlt_frame_set_alpha(frame, *buffer, size, NULL); + *buffer = mlt_properties_get_data(orig_props, "image", &size); + mlt_frame_set_image(frame, *buffer, size, NULL); + mlt_properties_set_data(frame_properties, + "avformat.conceal_error", + original, + 0, + (mlt_destructor) mlt_frame_close, + NULL); + *format = mlt_properties_get_int(orig_props, "format"); + set_image_size(self, width, height); + got_picture = 1; + } } // Regardless of speed, we expect to get the next frame (cos we ain't too bright) @@ -4476,6 +4499,7 @@ static int producer_get_frame(mlt_producer producer, mlt_frame_ptr frame, int in producer_avformat created = calloc(1, sizeof(struct producer_avformat_s)); if (!created) break; + created->last_good_cache_position = POSITION_INVALID; created->parent = producer; // The mutexes must exist before the state is visible to anyone else: it can // be evicted and destroyed, or used by the code below, right away. From 2fa2f02409ed95399f5c1cc05bd272baf667ceb8 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Tue, 27 Oct 2026 17:17:49 -0700 Subject: [PATCH 4/8] framework: add mlt_frame_get/set_resize_alpha filter_resize pads with the resize_alpha frame property, and until now a missing property and an explicit 0 meant the same thing because every reader and writer used get_int and set_int. To pad an opaque yuv422 image with transparency without converting it to rgba and back, filter_resize needs to tell an explicit request from the default. Add a getter that reports whether resize_alpha is set and a setter that can clear it, so that code that saves and restores the property keeps a missing value missing. copy_consumer_image_hints() now uses them: it previously wrote 0 onto every frame it copied hints to, which would have made every padded frame look like an explicit request. --- src/framework/mlt.vers | 2 ++ src/framework/mlt_frame.c | 45 ++++++++++++++++++++++++++++++++++++--- src/framework/mlt_frame.h | 2 ++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/framework/mlt.vers b/src/framework/mlt.vers index 0c46a13b6..f81f82a9e 100644 --- a/src/framework/mlt.vers +++ b/src/framework/mlt.vers @@ -709,4 +709,6 @@ MLT_7.42.0 { MLT_7.44.0 { global: mlt_properties_revision; + mlt_frame_get_resize_alpha; + mlt_frame_set_resize_alpha; } MLT_7.42.0; diff --git a/src/framework/mlt_frame.c b/src/framework/mlt_frame.c index 69680eb40..89ce94dd3 100644 --- a/src/framework/mlt_frame.c +++ b/src/framework/mlt_frame.c @@ -1030,15 +1030,54 @@ void mlt_frame_copy_convert_image(mlt_frame dst, mlt_frame src) NULL); } +/** Get the padding alpha requested of filter_resize. + * + * Missing and an explicit 0 are different: missing keeps the default of padding + * only an existing alpha mask, while an explicit value other than 255 also asks + * filter_resize to create a mask for an opaque image. Use this pair instead of + * get_int/set_int, which turn missing into 0. + * \public \memberof mlt_frame_s + * \param self a frame + * \param[out] value the padding alpha if set; may be NULL + * \return true if resize_alpha is set + */ + +int mlt_frame_get_resize_alpha(mlt_frame self, int *value) +{ + mlt_properties properties = MLT_FRAME_PROPERTIES(self); + int is_set = mlt_properties_exists(properties, "resize_alpha"); + if (value) + *value = is_set ? mlt_properties_get_int(properties, "resize_alpha") : 0; + return is_set; +} + +/** Set or clear the padding alpha requested of filter_resize. + * + * \public \memberof mlt_frame_s + * \param self a frame + * \param is_set false to remove the request + * \param value the padding alpha + * \see mlt_frame_get_resize_alpha + */ + +void mlt_frame_set_resize_alpha(mlt_frame self, int is_set, int value) +{ + mlt_properties properties = MLT_FRAME_PROPERTIES(self); + if (is_set) + mlt_properties_set_int(properties, "resize_alpha", value); + else + mlt_properties_clear(properties, "resize_alpha"); +} + /** Copy consumer scaling hints from \p src onto \p dst. */ static void copy_consumer_image_hints(mlt_frame dst, mlt_frame src) { mlt_properties dst_properties = MLT_FRAME_PROPERTIES(dst); mlt_properties src_properties = MLT_FRAME_PROPERTIES(src); + int resize_alpha; + int has_resize_alpha = mlt_frame_get_resize_alpha(src, &resize_alpha); - mlt_properties_set_int(dst_properties, - "resize_alpha", - mlt_properties_get_int(src_properties, "resize_alpha")); + mlt_frame_set_resize_alpha(dst, has_resize_alpha, resize_alpha); mlt_properties_set_int(dst_properties, "distort", mlt_properties_get_int(src_properties, "distort")); diff --git a/src/framework/mlt_frame.h b/src/framework/mlt_frame.h index 9958b5540..6cbec3919 100644 --- a/src/framework/mlt_frame.h +++ b/src/framework/mlt_frame.h @@ -197,6 +197,8 @@ MLT_EXPORT int mlt_frame_next_convert_image(mlt_frame self, mlt_image_format *format, mlt_image_format output); MLT_EXPORT void mlt_frame_copy_convert_image(mlt_frame dst, mlt_frame src); +MLT_EXPORT int mlt_frame_get_resize_alpha(mlt_frame self, int *value); +MLT_EXPORT void mlt_frame_set_resize_alpha(mlt_frame self, int is_set, int value); 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); From 3437a5ed6c43dd15b1475164687617e10a4a3e60 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Tue, 27 Oct 2026 17:17:49 -0700 Subject: [PATCH 5/8] core: request transparent padding with resize_alpha The luma transition marked both frames with a fix_background_alpha frame property so that filter_resize would give an opaque yuv422 image a mask and pad it with transparency. That coupled filter_resize to one transition through a property that means nothing to it. filter_resize now creates the mask when resize_alpha is explicitly set to a value other than 255, and pads with that value, which is what padding an rgba image did. The luma transition requests it by setting resize_alpha to 0 when fix_background_alpha is on and the frame has no request yet. The composite transition saves and restores resize_alpha around its b frame with mlt_frame_get/set_resize_alpha, so that a frame without the property does not leave with an explicit 0. Frames that never set resize_alpha are padded exactly as before, without a mask. --- src/modules/core/filter_resize.c | 11 +++++------ src/modules/core/transition_composite.c | 10 ++++++---- src/modules/core/transition_luma.c | 17 +++++++++++------ 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/modules/core/filter_resize.c b/src/modules/core/filter_resize.c index cc04ac947..2ef00752e 100644 --- a/src/modules/core/filter_resize.c +++ b/src/modules/core/filter_resize.c @@ -166,7 +166,9 @@ static uint8_t *frame_resize_image(mlt_frame frame, int owidth, int oheight, mlt oheight, mlt_image_format_name(format)); - uint8_t alpha_value = mlt_properties_get_int(properties, "resize_alpha"); + int resize_alpha_value; + int has_resize_alpha = mlt_frame_get_resize_alpha(frame, &resize_alpha_value); + uint8_t alpha_value = resize_alpha_value; // Create the output image uint8_t *output = mlt_pool_alloc(owidth * (oheight + 1) * bpp); @@ -176,11 +178,8 @@ static uint8_t *frame_resize_image(mlt_frame frame, int owidth, int oheight, mlt // Now update the frame mlt_frame_set_image(frame, output, owidth * (oheight + 1) * bpp, mlt_pool_release); - // An opaque image without an alpha mask gets one, so that the padding can be - // transparent. This gives the same result as padding an rgba image, without - // converting the image to rgba and back. - if (format == mlt_image_yuv422 && !alpha - && mlt_properties_get_int(properties, "fix_background_alpha")) { + // An explicit resize_alpha other than 255 needs a mask to make the padding transparent. + if (format == mlt_image_yuv422 && !alpha && alpha_value != 255 && has_resize_alpha) { alpha_size = iwidth * iheight; alpha = mlt_pool_alloc(alpha_size); memset(alpha, 255, alpha_size); diff --git a/src/modules/core/transition_composite.c b/src/modules/core/transition_composite.c index 1733f2573..94af52f95 100644 --- a/src/modules/core/transition_composite.c +++ b/src/modules/core/transition_composite.c @@ -1,6 +1,6 @@ /* * transition_composite.c -- compose one image over another using alpha channel - * 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 @@ -722,7 +722,9 @@ static int get_b_frame_image(mlt_transition self, // Get the properties objects mlt_properties b_props = MLT_FRAME_PROPERTIES(b_frame); mlt_properties properties = MLT_TRANSITION_PROPERTIES(self); - uint8_t resize_alpha = mlt_properties_get_int(b_props, "resize_alpha"); + int saved_resize_alpha; + int has_resize_alpha = mlt_frame_get_resize_alpha(b_frame, &saved_resize_alpha); + uint8_t resize_alpha = saved_resize_alpha; double output_ar = mlt_profile_sar(mlt_service_profile(MLT_TRANSITION_SERVICE(self))); // Do not scale if we are cropping - the compositing rectangle can crop the b image @@ -813,7 +815,7 @@ static int get_b_frame_image(mlt_transition self, // If we're not aligned, we want a non-transparent background if (mlt_properties_get_int(properties, "aligned") == 0) - mlt_properties_set_int(b_props, "resize_alpha", 255); + mlt_frame_set_resize_alpha(b_frame, 1, 255); // Take into consideration alignment for optimisation (titles are a special case) if (!mlt_properties_get_int(properties, "titles") @@ -835,7 +837,7 @@ static int get_b_frame_image(mlt_transition self, geometry->sw = *width; // Set the frame back - mlt_properties_set_int(b_props, "resize_alpha", resize_alpha); + mlt_frame_set_resize_alpha(b_frame, has_resize_alpha, saved_resize_alpha); return !error && image; } diff --git a/src/modules/core/transition_luma.c b/src/modules/core/transition_luma.c index 397eb2607..c1f56190f 100644 --- a/src/modules/core/transition_luma.c +++ b/src/modules/core/transition_luma.c @@ -138,6 +138,13 @@ static uint8_t *add_opaque_alpha(mlt_frame frame, int width, int height) return alpha; } +// Makes filter_resize pad with transparency as it does for rgba, unless already specified. +static void request_transparent_padding(mlt_frame frame) +{ + if (!mlt_frame_get_resize_alpha(frame, NULL)) + mlt_frame_set_resize_alpha(frame, 1, 0); +} + struct dissolve_opaque_context { uint8_t *dst_image; @@ -200,9 +207,8 @@ static inline int dissolve_yuv422(mlt_frame frame, int mix = weight * (1 << 16); if (fix_background_alpha) { - // Tell filter_resize to make the padding transparent (see filter_resize.c). - mlt_properties_set_int(MLT_FRAME_PROPERTIES(frame), "fix_background_alpha", 1); - mlt_properties_set_int(MLT_FRAME_PROPERTIES(that), "fix_background_alpha", 1); + request_transparent_padding(frame); + request_transparent_padding(that); } mlt_frame_get_image(frame, &p_dest, &format, &width, &height, 1); @@ -411,9 +417,8 @@ static void luma_composite_yuv422(mlt_frame a_frame, int stride_dest; if (fix_background_alpha) { - // Tell filter_resize to make the padding transparent (see filter_resize.c). - mlt_properties_set_int(MLT_FRAME_PROPERTIES(a_frame), "fix_background_alpha", 1); - mlt_properties_set_int(MLT_FRAME_PROPERTIES(b_frame), "fix_background_alpha", 1); + request_transparent_padding(a_frame); + request_transparent_padding(b_frame); } if (mlt_properties_get(&a_frame->parent, "distort")) From 8c3b8941f52b2bfb9d9aabe8b62d8b441f8eac74 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Tue, 27 Oct 2026 17:18:25 -0700 Subject: [PATCH 6/8] luma: share the yuv422 frame setup and fix mismatched sizes dissolve_yuv422() and luma_composite_yuv422() each requested transparent padding, fetched both images as yuv422, checked both alpha masks, and added an opaque mask to one side when the other was translucent. The order matters: padding must be requested before the images are fetched, and the opaque mask must be added only when the other side is translucent, or the result keeps a fully opaque mask and qtblend goes back to rgba. Both now call prepare_yuv422_pair(). The dissolve also uses one slice function instead of two. An alpha_over flag picks composite_line_yuv_float() or composite_line_yuv() per line, and each image advances by its own width. This changes the output when the two frames differ in size, which contradicts "The output does not change" in "luma: dissolve opaque yuv422 frames in slices". Before, the opaque dissolve stepped the source by the width clamped to the destination and ran over the full destination height, and the alpha-over dissolve used that clamped width as the stride of both images. A wider source was then read at the wrong rows, and a shorter one was read past its end. The luma wipe had the same source stride problem. Now both blend only the overlapping area, each image with its own stride, and the alpha masks skip the rest of each row. When the sizes are equal, the output does not change. --- src/modules/core/transition_luma.c | 266 ++++++++++++++--------------- 1 file changed, 129 insertions(+), 137 deletions(-) diff --git a/src/modules/core/transition_luma.c b/src/modules/core/transition_luma.c index c1f56190f..61e18ac71 100644 --- a/src/modules/core/transition_luma.c +++ b/src/modules/core/transition_luma.c @@ -17,7 +17,7 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include @@ -95,35 +95,49 @@ struct dissolve_slice_context int width; int height; float weight; + // The following are used only by dissolve_slice_yuv422. + int dst_stride; // in pixels + int src_stride; // in pixels + int mix; + int alpha_over; }; static int dissolve_slice_yuv422(int id, int index, int count, void *context) { struct dissolve_slice_context ctx = *((struct dissolve_slice_context *) context); - int stride = ctx.width * 2; int slice_start, slice_height = mlt_slices_size_slice(count, index, ctx.height, &slice_start); - int i; - ctx.dst_image += slice_start * stride; - ctx.src_image += slice_start * stride; + ctx.dst_image += slice_start * ctx.dst_stride * 2; + ctx.src_image += slice_start * ctx.src_stride * 2; if (ctx.dst_alpha) - ctx.dst_alpha += slice_start * ctx.width; + ctx.dst_alpha += slice_start * ctx.dst_stride; if (ctx.src_alpha) - ctx.src_alpha += slice_start * ctx.width; - - for (i = 0; i < slice_height; i++) { - composite_line_yuv_float(ctx.dst_image, - ctx.src_image, - ctx.width, - ctx.src_alpha, - ctx.dst_alpha, - ctx.weight); - ctx.dst_image += stride; - ctx.src_image += stride; + ctx.src_alpha += slice_start * ctx.src_stride; + + for (int i = 0; i < slice_height; i++) { + if (ctx.alpha_over) + composite_line_yuv_float(ctx.dst_image, + ctx.src_image, + ctx.width, + ctx.src_alpha, + ctx.dst_alpha, + ctx.weight); + else + composite_line_yuv(ctx.dst_image, + ctx.src_image, + ctx.width, + ctx.src_alpha, + ctx.dst_alpha, + ctx.mix, + NULL, + 0, + 0); + ctx.dst_image += ctx.dst_stride * 2; + ctx.src_image += ctx.src_stride * 2; if (ctx.dst_alpha) - ctx.dst_alpha += ctx.width; + ctx.dst_alpha += ctx.dst_stride; if (ctx.src_alpha) - ctx.src_alpha += ctx.width; + ctx.src_alpha += ctx.src_stride; } return 0; } @@ -138,55 +152,54 @@ static uint8_t *add_opaque_alpha(mlt_frame frame, int width, int height) return alpha; } -// Makes filter_resize pad with transparency as it does for rgba, unless already specified. +// Makes filter_resize pad with transparency as it does for rgba, unless already +// specified. static void request_transparent_padding(mlt_frame frame) { if (!mlt_frame_get_resize_alpha(frame, NULL)) mlt_frame_set_resize_alpha(frame, 1, 0); } -struct dissolve_opaque_context +/* Fetch both frames as yuv422. Padding is requested before either fetch. + * A side with no mask gets an opaque one only when fix_background_alpha is on + * and the other side is translucent. + */ +static void prepare_yuv422_pair(mlt_frame dst_frame, + mlt_frame src_frame, + int *dst_width, + int *dst_height, + int *src_width, + int *src_height, + int fix_background_alpha, + uint8_t **dst_image, + uint8_t **src_image, + uint8_t **dst_alpha, + uint8_t **src_alpha, + int *is_translucent) { - uint8_t *dst_image; - uint8_t *src_image; - uint8_t *dst_alpha; - uint8_t *src_alpha; - int dst_width; - int src_width; - int height; - int mix; -}; + mlt_image_format format = mlt_image_yuv422; -static int dissolve_opaque_slice_yuv422(int id, int index, int count, void *context) -{ - struct dissolve_opaque_context ctx = *((struct dissolve_opaque_context *) context); - int slice_start, slice_height = mlt_slices_size_slice(count, index, ctx.height, &slice_start); + if (fix_background_alpha) { + request_transparent_padding(dst_frame); + request_transparent_padding(src_frame); + } - ctx.dst_image += slice_start * (ctx.dst_width << 1); - ctx.src_image += slice_start * (ctx.src_width << 1); - if (ctx.dst_alpha) - ctx.dst_alpha += slice_start * ctx.dst_width; - if (ctx.src_alpha) - ctx.src_alpha += slice_start * ctx.src_width; + mlt_frame_get_image(dst_frame, dst_image, &format, dst_width, dst_height, 1); + *dst_alpha = mlt_frame_get_alpha(dst_frame); + format = mlt_image_yuv422; + mlt_frame_get_image(src_frame, src_image, &format, src_width, src_height, 0); + *src_alpha = mlt_frame_get_alpha(src_frame); + *is_translucent = (*dst_alpha && !is_opaque(*dst_alpha, *dst_width, *dst_height)) + || (*src_alpha && !is_opaque(*src_alpha, *src_width, *src_height)); - for (int i = 0; i < slice_height; i++) { - composite_line_yuv(ctx.dst_image, - ctx.src_image, - ctx.src_width, - ctx.src_alpha, - ctx.dst_alpha, - ctx.mix, - NULL, - 0, - 0); - ctx.dst_image += ctx.dst_width << 1; - ctx.src_image += ctx.src_width << 1; - if (ctx.dst_alpha) - ctx.dst_alpha += ctx.dst_width; - if (ctx.src_alpha) - ctx.src_alpha += ctx.src_width; + // An opaque frame without an alpha mask must get one when the other frame is + // translucent, so that the alpha of the result is computed (see alpha_over). + if (fix_background_alpha && *is_translucent) { + if (!*dst_alpha) + *dst_alpha = add_opaque_alpha(dst_frame, *dst_width, *dst_height); + if (!*src_alpha) + *src_alpha = add_opaque_alpha(src_frame, *src_width, *src_height); } - return 0; } static inline int dissolve_yuv422(mlt_frame frame, @@ -200,58 +213,38 @@ static inline int dissolve_yuv422(mlt_frame frame, { int ret = 0; int width_src = width, height_src = height; - mlt_image_format format = mlt_image_yuv422; uint8_t *p_src, *p_dest; uint8_t *alpha_src; uint8_t *alpha_dst; + int is_translucent = 0; int mix = weight * (1 << 16); - if (fix_background_alpha) { - request_transparent_padding(frame); - request_transparent_padding(that); - } - - mlt_frame_get_image(frame, &p_dest, &format, &width, &height, 1); - alpha_dst = mlt_frame_get_alpha(frame); - format = mlt_image_yuv422; - mlt_frame_get_image(that, &p_src, &format, &width_src, &height_src, 0); - 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)); - - // An opaque frame without an alpha mask must get one when the other frame is - // translucent, so that the alpha of the result is computed (see alpha_over). - if (fix_background_alpha && is_translucent) { - if (!alpha_dst) - alpha_dst = add_opaque_alpha(frame, width, height); - if (!alpha_src) - alpha_src = add_opaque_alpha(that, width_src, height_src); - } - - // Pick the lesser of two evils ;-) - width_src = width_src > width ? width : width_src; - height_src = height_src > height ? height : height_src; - - if (is_translucent && alpha_over) { - struct dissolve_slice_context context = {.dst_image = p_dest, - .src_image = p_src, - .dst_alpha = alpha_dst, - .src_alpha = alpha_src, - .width = width_src, - .height = height_src, - .weight = weight}; - mlt_slices_run_normal(threads, dissolve_slice_yuv422, &context); - } else { - struct dissolve_opaque_context context = {.dst_image = p_dest, - .src_image = p_src, - .dst_alpha = alpha_dst, - .src_alpha = alpha_src, - .dst_width = width, - .src_width = width_src, - .height = height, - .mix = mix}; - mlt_slices_run_normal(threads, dissolve_opaque_slice_yuv422, &context); - } + prepare_yuv422_pair(frame, + that, + &width, + &height, + &width_src, + &height_src, + fix_background_alpha, + &p_dest, + &p_src, + &alpha_dst, + &alpha_src, + &is_translucent); + + struct dissolve_slice_context context = {.dst_image = p_dest, + .src_image = p_src, + .dst_alpha = alpha_dst, + .src_alpha = alpha_src, + // Pick the lesser of two evils ;-) + .width = MIN(width_src, width), + .height = MIN(height_src, height), + .weight = weight, + .dst_stride = width, + .src_stride = width_src, + .mix = mix, + .alpha_over = is_translucent && alpha_over}; + mlt_slices_run_normal(threads, dissolve_slice_yuv422, &context); return ret; } @@ -359,7 +352,7 @@ static inline int dissolve_rgba64( } /** A smoother, non-linear threshold determination function. -*/ + */ static inline int32_t smoothstep(int32_t edge1, int32_t edge2, uint32_t a) { @@ -389,7 +382,8 @@ static float smoothstep_float(float edge1, float edge2, float a) /** powerful stuff - \param field_order -1 = progressive, 0 = lower field first, 1 = top field first + \param field_order -1 = progressive, 0 = lower field first, 1 = top field + first */ static void luma_composite_yuv422(mlt_frame a_frame, mlt_frame b_frame, @@ -408,46 +402,37 @@ 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 = mlt_image_yuv422; - mlt_image_format format_dest = mlt_image_yuv422; uint8_t *p_src, *p_dest; uint8_t *alpha_src, *alpha_dest; int i, j; int stride_src; int stride_dest; - - if (fix_background_alpha) { - request_transparent_padding(a_frame); - request_transparent_padding(b_frame); - } + int is_translucent = 0; if (mlt_properties_get(&a_frame->parent, "distort")) mlt_properties_set(&b_frame->parent, "distort", mlt_properties_get(&a_frame->parent, "distort")); - mlt_frame_get_image(a_frame, &p_dest, &format_dest, &width_dest, &height_dest, 1); - alpha_dest = mlt_frame_get_alpha(a_frame); - mlt_frame_get_image(b_frame, &p_src, &format_src, &width_src, &height_src, 0); - alpha_src = mlt_frame_get_alpha(b_frame); + + prepare_yuv422_pair(a_frame, + b_frame, + &width_dest, + &height_dest, + &width_src, + &height_src, + fix_background_alpha, + &p_dest, + &p_src, + &alpha_dest, + &alpha_src, + &is_translucent); if (*width == 0 || *height == 0) return; - int is_translucent = (alpha_dest && !is_opaque(alpha_dest, width_dest, height_dest)) - || (alpha_src && !is_opaque(alpha_src, width_src, height_src)); - - // An opaque frame without an alpha mask must get one when the other frame is - // translucent, so that the alpha of the result is computed (see alpha_over). - if (fix_background_alpha && is_translucent) { - if (!alpha_dest) - alpha_dest = add_opaque_alpha(a_frame, width_dest, height_dest); - if (!alpha_src) - alpha_src = add_opaque_alpha(b_frame, width_src, height_src); - } - - // Pick the lesser of two evils ;-) - width_src = width_src > width_dest ? width_dest : width_src; - height_src = height_src > height_dest ? height_dest : height_src; + // Blend the overlap. Each image keeps its own full width as the row stride. + int blend_width = MIN(width_src, width_dest); + int blend_height = MIN(height_src, height_dest); stride_src = width_src * 2; stride_dest = width_dest * 2; @@ -480,12 +465,12 @@ static void luma_composite_yuv422(mlt_frame a_frame, y_offset = field << 16; i = field; - while (i < height_src) { + while (i < blend_height) { p = p_row; q = q_row; l = luma_bitmap + (y_offset >> 16) * (luma_width * field_count); x_offset = 0; - j = width_src; + j = blend_width; if (is_translucent) { while (j--) { @@ -535,6 +520,12 @@ static void luma_composite_yuv422(mlt_frame a_frame, x_offset += x_diff; } } + if (is_translucent) { + if (alpha_dest) + alpha_dest += width_dest - blend_width; + if (alpha_src) + alpha_src += width_src - blend_width; + } y_offset += y_diff; i += field_count; p_row += field_stride_src; @@ -1097,7 +1088,8 @@ static int transition_get_image(mlt_frame a_frame, if (mlt_color_trc_linear == mlt_image_color_trc_id(mlt_properties_get(a_props, "consumer.mlt_color_trc"))) { - // Make progress appear linear using a gamma curve similar to sRGB or Rec. 709 + // Make progress appear linear using a gamma curve similar to sRGB or Rec. + // 709 if (reverse || invert) mix = 1.0 - pow(1.0 - mix, 2.0); else @@ -1139,7 +1131,7 @@ static int transition_get_image(mlt_frame a_frame, } /** Luma transition processing. -*/ + */ static mlt_frame transition_process(mlt_transition transition, mlt_frame a_frame, mlt_frame b_frame) { @@ -1156,7 +1148,7 @@ static mlt_frame transition_process(mlt_transition transition, mlt_frame a_frame } /** Constructor for the filter. -*/ + */ mlt_transition transition_luma_init(mlt_profile profile, mlt_service_type type, From bf7e06bf70b94c3f9cf70412598208d77facc388 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Tue, 27 Oct 2026 17:18:25 -0700 Subject: [PATCH 7/8] avformat: move the error concealment state into conceal.c The last good frame for error concealment was kept in three fields of the producer and updated in several places in producer_get_image(): before a cache put of an earlier frame and before a cache reset, the cached frame had to be copied out, or a later decoding failure would find nothing to show. Forgetting one of those copies would silently break concealment. Move the state and its transitions into a small avformat_conceal type with record, detach, fetch, and close functions, which are now the only code that changes it. The picture is either an owned frame, once it has been copied out of the image cache, or otherwise the image cache key. The behavior does not change. --- src/modules/avformat/CMakeLists.txt | 2 + src/modules/avformat/conceal.c | 86 ++++++++++++++++++++++++ src/modules/avformat/conceal.h | 74 ++++++++++++++++++++ src/modules/avformat/producer_avformat.c | 47 ++++--------- 4 files changed, 176 insertions(+), 33 deletions(-) create mode 100644 src/modules/avformat/conceal.c create mode 100644 src/modules/avformat/conceal.h diff --git a/src/modules/avformat/CMakeLists.txt b/src/modules/avformat/CMakeLists.txt index 97d3420d4..c0579d801 100644 --- a/src/modules/avformat/CMakeLists.txt +++ b/src/modules/avformat/CMakeLists.txt @@ -7,6 +7,8 @@ add_library(mltavformat MODULE filter_sws_colortransform.c # avformat producer_avformat.c + conceal.c + conceal.h consumer_avformat.c # avfilter filter_avfilter.c diff --git a/src/modules/avformat/conceal.c b/src/modules/avformat/conceal.c new file mode 100644 index 000000000..1b49d8579 --- /dev/null +++ b/src/modules/avformat/conceal.c @@ -0,0 +1,86 @@ +/* + * conceal.c -- last good picture for avformat error concealment + * 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 + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "conceal.h" + +/* Not a valid image-cache key. Zero is a valid key, so the empty state cannot be 0. */ +#define CONCEAL_NO_CACHE_POSITION ((mlt_position) -1) + +void avformat_conceal_init(avformat_conceal *self) +{ + self->owned = NULL; + self->cache_position = CONCEAL_NO_CACHE_POSITION; + /* Same initial watermark as the previous zeroed int: position 0 is recorded. */ + self->source_position = 0; +} + +void avformat_conceal_close(avformat_conceal *self) +{ + if (self->owned) + mlt_frame_close(self->owned); + self->owned = NULL; + self->cache_position = CONCEAL_NO_CACHE_POSITION; +} + +void avformat_conceal_record(avformat_conceal *self, + mlt_frame frame, + mlt_cache cache, + int source_position, + mlt_position cache_position) +{ + if (source_position < self->source_position) + return; + + self->source_position = source_position; + if (self->owned) + mlt_frame_close(self->owned); + if (cache) { + self->owned = NULL; + self->cache_position = cache_position; + } else { + self->owned = mlt_frame_clone(frame, 1); + self->cache_position = CONCEAL_NO_CACHE_POSITION; + } +} + +void avformat_conceal_detach(avformat_conceal *self, mlt_cache cache) +{ + if (!self->owned && cache && self->cache_position != CONCEAL_NO_CACHE_POSITION) + self->owned = mlt_cache_get_frame(cache, self->cache_position); +} + +void avformat_conceal_detach_if_older(avformat_conceal *self, mlt_cache cache, int source_position) +{ + if (source_position < self->source_position) + avformat_conceal_detach(self, cache); +} + +mlt_frame avformat_conceal_fetch(avformat_conceal *self, mlt_cache cache) +{ + if (self->owned) + return mlt_frame_clone(self->owned, 1); + if (cache && self->cache_position != CONCEAL_NO_CACHE_POSITION) + return mlt_cache_get_frame(cache, self->cache_position); + return NULL; +} + +void avformat_conceal_reset_source(avformat_conceal *self) +{ + self->source_position = -1; +} diff --git a/src/modules/avformat/conceal.h b/src/modules/avformat/conceal.h new file mode 100644 index 000000000..0b92420ef --- /dev/null +++ b/src/modules/avformat/conceal.h @@ -0,0 +1,74 @@ +/* + * conceal.h -- last good picture for avformat error concealment + * 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 + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef AVFORMAT_CONCEAL_H +#define AVFORMAT_CONCEAL_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* The last good picture is either an owned frame or a cache key, never both + * as the source of a fetch. Once the picture has been detached, owned is set + * and fetch clones that frame. Otherwise owned is NULL and fetch loads + * cache_position from the image cache. source_position is only the watermark + * that tells record() whether a newer success may replace the picture. + * While the producer is open, record and detach are the only writers of the + * picture. fetch does not modify it. close releases the owner. reset_source + * moves only the watermark. + */ +typedef struct avformat_conceal_s +{ + mlt_frame owned; + mlt_position cache_position; + int source_position; +} avformat_conceal; + +void avformat_conceal_init(avformat_conceal *self); +void avformat_conceal_close(avformat_conceal *self); + +/* Remember a successful frame. A source_position older than the watermark + * does not replace the picture. With a cache, only the cache key is kept. + * Without one, a deep copy is owned. + */ +void avformat_conceal_record(avformat_conceal *self, + mlt_frame frame, + mlt_cache cache, + int source_position, + mlt_position cache_position); + +/* Copy the picture out of the cache before a put or a cache reset can drop it. */ +void avformat_conceal_detach(avformat_conceal *self, mlt_cache cache); + +/* Detach when this frame is older than the watermark, so caching it can evict. */ +void avformat_conceal_detach_if_older(avformat_conceal *self, mlt_cache cache, int source_position); + +/* A new frame the caller closes, or NULL. Prefers the owned frame after detach. */ +mlt_frame avformat_conceal_fetch(avformat_conceal *self, mlt_cache cache); + +/* Allow the next record() to replace the picture without dropping the one held now. */ +void avformat_conceal_reset_source(avformat_conceal *self); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/modules/avformat/producer_avformat.c b/src/modules/avformat/producer_avformat.c index 389f78366..b638448d4 100644 --- a/src/modules/avformat/producer_avformat.c +++ b/src/modules/avformat/producer_avformat.c @@ -27,6 +27,7 @@ #endif #include "common.h" +#include "conceal.h" // MLT Header files #include @@ -129,10 +130,7 @@ struct producer_avformat_s int packets_thread_stop; // non-zero when packets_thread is to stop int is_thread_init; AVRational video_time_base; - mlt_frame last_good_frame; // for video error concealment - int last_good_position; // for video error concealment - // Image cache key of the last good frame when last_good_frame is NULL. - mlt_position last_good_cache_position; + avformat_conceal conceal; // last good picture for video error concealment AVFilterGraph *vfilter_graph; AVFilterContext *vfilter_in; AVFilterContext *vfilter_out; @@ -232,7 +230,7 @@ mlt_producer producer_avformat_init(mlt_profile profile, const char *service, ch if (file) { // Construct the producer producer_avformat self = calloc(1, sizeof(struct producer_avformat_s)); - self->last_good_cache_position = POSITION_INVALID; + avformat_conceal_init(&self->conceal); producer = calloc(1, sizeof(struct mlt_producer_s)); // Initialise it @@ -2758,15 +2756,6 @@ static void init_cache(mlt_properties properties, mlt_cache *cache) mlt_cache_set_size(*cache, cache_size); } -// Copy the last good frame out of the image cache before the cache can lose it. -static void keep_last_good_frame(producer_avformat self) -{ - if (!self->last_good_frame && self->image_cache - && self->last_good_cache_position != POSITION_INVALID) - self->last_good_frame = mlt_cache_get_frame(self->image_cache, - self->last_good_cache_position); -} - /** Get an image from a frame. */ @@ -2813,7 +2802,7 @@ static int producer_get_image(mlt_frame frame, if (self->reset_image_cache) { self->reset_image_cache = 0; - keep_last_good_frame(self); + avformat_conceal_detach(&self->conceal, self->image_cache); mlt_cache_close(self->image_cache); self->image_cache = NULL; av_frame_free(&self->video_frame); @@ -3046,7 +3035,7 @@ static int producer_get_image(mlt_frame frame, mlt_log_warning(MLT_PRODUCER_SERVICE(producer), "video decoding error %d\n", error); - self->last_good_position = POSITION_INVALID; + avformat_conceal_reset_source(&self->conceal); } } else { if (self->hwaccel.device_ctx @@ -3254,8 +3243,7 @@ static int producer_get_image(mlt_frame frame, if (image_size > 0) { mlt_properties_set_int(frame_properties, "format", *format); // An earlier frame does not become the last good one, but caching it may evict that. - if (self->current_position < self->last_good_position) - keep_last_good_frame(self); + avformat_conceal_detach_if_older(&self->conceal, self->image_cache, self->current_position); // Cache the image for rapid repeated access. if (self->image_cache) { if (is_album_art(self)) { @@ -3268,20 +3256,14 @@ static int producer_get_image(mlt_frame frame, } } // For error concealment; avoid a second copy when the image cache already has one. - if (self->current_position >= self->last_good_position) { - self->last_good_position = self->current_position; - if (self->last_good_frame) - mlt_frame_close(self->last_good_frame); - self->last_good_frame = self->image_cache ? NULL : mlt_frame_clone(frame, 1); - self->last_good_cache_position = self->image_cache ? position : POSITION_INVALID; - } + avformat_conceal_record(&self->conceal, + frame, + self->image_cache, + self->current_position, + position); } else { // Use last known good frame if there was a decoding failure. - mlt_frame original = NULL; - if (self->last_good_frame) - original = mlt_frame_clone(self->last_good_frame, 1); - else if (self->image_cache && self->last_good_cache_position != POSITION_INVALID) - original = mlt_cache_get_frame(self->image_cache, self->last_good_cache_position); + mlt_frame original = avformat_conceal_fetch(&self->conceal, self->image_cache); if (original) { mlt_properties orig_props = MLT_FRAME_PROPERTIES(original); int size = 0; @@ -4499,7 +4481,7 @@ static int producer_get_frame(mlt_producer producer, mlt_frame_ptr frame, int in producer_avformat created = calloc(1, sizeof(struct producer_avformat_s)); if (!created) break; - created->last_good_cache_position = POSITION_INVALID; + avformat_conceal_init(&created->conceal); created->parent = producer; // The mutexes must exist before the state is visible to anyone else: it can // be evicted and destroyed, or used by the code below, right away. @@ -4678,8 +4660,7 @@ static void producer_avformat_close(producer_avformat self) // Cleanup caches. mlt_cache_close(self->image_cache); mlt_cache_close(self->audio_cache); - if (self->last_good_frame) - mlt_frame_close(self->last_good_frame); + avformat_conceal_close(&self->conceal); // Cleanup the mutexes if (self->is_mutex_init) { From 46ab3b2f728cdb3952c59d9c2b01e3bef75902d0 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Tue, 27 Oct 2026 17:18:25 -0700 Subject: [PATCH 8/8] tests: add test_dissolve Covers filter_resize padding with a missing, an explicit 0, and an explicit 255 resize_alpha; that copying consumer hints and the composite transition keep a missing resize_alpha missing; the luma dissolve and wipe with equal and mismatched frame sizes; and the avformat error concealment state for forward playback, a cache eviction after detach, and backward playback. --- src/tests/CMakeLists.txt | 5 + src/tests/test_dissolve/test_dissolve.cpp | 535 ++++++++++++++++++++++ 2 files changed, 540 insertions(+) create mode 100644 src/tests/test_dissolve/test_dissolve.cpp diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 22ab36b58..a80518de8 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -47,6 +47,11 @@ add_qt_test(TEST_NAME repository) add_qt_test(TEST_NAME service) add_qt_test(TEST_NAME tractor) add_qt_test(TEST_NAME xml) +add_qt_test( + TEST_NAME dissolve + INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/src/modules/avformat" + SOURCE_FILES "${CMAKE_SOURCE_DIR}/src/modules/avformat/conceal.c" +) if(MOD_AVFORMAT) add_qt_test(TEST_NAME mod_avformat) diff --git a/src/tests/test_dissolve/test_dissolve.cpp b/src/tests/test_dissolve/test_dissolve.cpp new file mode 100644 index 000000000..4f2aa10bc --- /dev/null +++ b/src/tests/test_dissolve/test_dissolve.cpp @@ -0,0 +1,535 @@ +/* + * 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 + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "conceal.h" + +#include +#include +#include +#include +#include + +using namespace Mlt; + +namespace { + +struct AlphaCapture +{ + int did; + int exists; + int value; +}; + +struct YuvSource +{ + int width; + int height; + int fill; + int perRow; + AlphaCapture *capture; +}; + +struct CopiedImage +{ + int width; + int height; + int hasAlpha; + uint8_t pixels[16 * 16 * 2]; + uint8_t alpha[16 * 16]; +}; + +int yuv422FromSpec(mlt_frame frame, + uint8_t **image, + mlt_image_format *format, + int *width, + int *height, + int writable) +{ + Q_UNUSED(writable) + YuvSource *spec = (YuvSource *) mlt_properties_get_data(MLT_FRAME_PROPERTIES(frame), + "yuv_source", + NULL); + if (!spec || spec->width < 1 || spec->height < 1) + return 1; + + const int size = spec->width * spec->height * 2; + uint8_t *buffer = (uint8_t *) mlt_pool_alloc(size); + if (!buffer) + return 1; + for (int y = 0; y < spec->height; y++) { + const int value = spec->perRow ? spec->fill + y * 10 : spec->fill; + memset(buffer + y * spec->width * 2, value, spec->width * 2); + } + mlt_frame_set_image(frame, buffer, size, mlt_pool_release); + *image = buffer; + *format = mlt_image_yuv422; + *width = spec->width; + *height = spec->height; + if (spec->capture) { + mlt_properties properties = MLT_FRAME_PROPERTIES(frame); + spec->capture->did = 1; + spec->capture->exists = mlt_properties_exists(properties, "resize_alpha"); + spec->capture->value = mlt_properties_get_int(properties, "resize_alpha"); + } + return 0; +} + +void attachSource(mlt_frame frame, YuvSource *spec) +{ + mlt_properties_set_data(MLT_FRAME_PROPERTIES(frame), "yuv_source", spec, 0, NULL, NULL); + mlt_frame_push_get_image(frame, yuv422FromSpec); +} + +int yAt(const CopiedImage *image, int x, int y) +{ + return image->pixels[(y * image->width + x) * 2]; +} + +int alphaAt(const CopiedImage *image, int x, int y) +{ + return image->alpha[y * image->width + x]; +} + +// composite_line_yuv at mix 0.5 with a missing alpha mask. Weight is 0.5 * 65536. +int halfMix(int dest, int src) +{ + const int mix = 32768; + return (src * mix + dest * ((1 << 16) - mix)) >> 16; +} + +void pass(const char *name) +{ + printf("PASS %s\n", name); + fflush(stdout); +} + +mlt_frame makePicture(int marker, mlt_position key) +{ + mlt_frame frame = mlt_frame_init(NULL); + const int size = 8 * 8 * 2; + uint8_t *image = (uint8_t *) mlt_pool_alloc(size); + memset(image, marker, size); + mlt_frame_set_image(frame, image, size, mlt_pool_release); + mlt_properties properties = MLT_FRAME_PROPERTIES(frame); + mlt_properties_set_int(properties, "format", mlt_image_yuv422); + mlt_properties_set_int(properties, "width", 8); + mlt_properties_set_int(properties, "height", 8); + mlt_properties_set_position(properties, "original_position", key); + return frame; +} + +int pictureMarker(mlt_frame frame) +{ + int size = 0; + uint8_t *image = (uint8_t *) mlt_properties_get_data(MLT_FRAME_PROPERTIES(frame), + "image", + &size); + if (!image || size < 1) + return -1; + return image[0]; +} + +} // namespace + +class TestDissolve : public QObject +{ + Q_OBJECT + +public: + // Factory::close faults while unloading unrelated modules on this Windows + // build. Sibling tests such as test_filter leave that cleanup to process exit. + TestDissolve() { Factory::init(); } + +private: + CopiedImage fetch(mlt_frame frame, int width, int height) + { + CopiedImage copy; + memset(©, 0, sizeof(copy)); + mlt_image_format format = mlt_image_yuv422; + uint8_t *image = NULL; + int error = mlt_frame_get_image(frame, &image, &format, &width, &height, 1); + if (error || !image || width < 1 || height < 1 || width > 16 || height > 16) + return copy; + copy.width = width; + copy.height = height; + memcpy(copy.pixels, image, width * height * 2); + uint8_t *alpha = mlt_frame_get_alpha(frame); + if (alpha) { + copy.hasAlpha = 1; + memcpy(copy.alpha, alpha, width * height); + } + return copy; + } + + CopiedImage runResize(int setAlpha, int alphaValue) + { + Profile profile; + mlt_frame frame = mlt_frame_init(NULL); + YuvSource spec = {8, 8, 80, 0, NULL}; + attachSource(frame, &spec); + mlt_properties_set_int(MLT_FRAME_PROPERTIES(frame), "distort", 1); + if (setAlpha) + mlt_properties_set_int(MLT_FRAME_PROPERTIES(frame), "resize_alpha", alphaValue); + + mlt_filter filter = mlt_factory_filter(profile.get_profile(), "resize", NULL); + if (!filter) { + mlt_frame_close(frame); + return CopiedImage(); + } + mlt_filter_process(filter, frame); + CopiedImage image = fetch(frame, 16, 16); + mlt_filter_close(filter); + mlt_frame_close(frame); + return image; + } + + CopiedImage runLuma( + YuvSource *dest, YuvSource *src, int requestWidth, int requestHeight, int wipe) + { + Profile profile; + mlt_transition transition = mlt_factory_transition(profile.get_profile(), "luma", NULL); + CopiedImage image; + memset(&image, 0, sizeof(image)); + if (!transition) + return image; + + mlt_properties properties = MLT_TRANSITION_PROPERTIES(transition); + if (mlt_properties_get(properties, "resource")) + mlt_properties_clear(properties, "resource"); + mlt_properties_set_double(properties, "fixed", 0.5); + mlt_properties_set_int(properties, "fix_background_alpha", 1); + mlt_properties_set_int(properties, "alpha_over", 0); + mlt_properties_set_int(properties, "progressive", 1); + mlt_properties_set_int(properties, "threads", 1); + mlt_properties_set_double(properties, "softness", 0); + mlt_properties_set_int(properties, "in", 0); + mlt_properties_set_int(properties, "out", 10); + if (wipe) { + const int count = requestWidth * requestHeight; + uint16_t *bitmap = (uint16_t *) mlt_pool_alloc(count * sizeof(uint16_t)); + memset(bitmap, 0, count * sizeof(uint16_t)); + mlt_properties_set_int(properties, "width", requestWidth); + mlt_properties_set_int(properties, "height", requestHeight); + mlt_properties_set_data(properties, + "bitmap", + bitmap, + count * (int) sizeof(uint16_t), + mlt_pool_release, + NULL); + } + + mlt_frame a = mlt_frame_init(NULL); + mlt_frame b = mlt_frame_init(NULL); + attachSource(a, dest); + attachSource(b, src); + mlt_transition_process(transition, a, b); + image = fetch(a, requestWidth, requestHeight); + mlt_frame_close(b); + mlt_frame_close(a); + mlt_transition_close(transition); + return image; + } + +private Q_SLOTS: + void resizeAlphaMask() + { + CopiedImage missing = runResize(0, 0); + QCOMPARE(missing.width, 16); + QCOMPARE(missing.height, 16); + QCOMPARE(missing.hasAlpha, 0); + pass("resizeAlphaMask missing"); + + CopiedImage transparent = runResize(1, 0); + QCOMPARE(transparent.width, 16); + QCOMPARE(transparent.height, 16); + QCOMPARE(transparent.hasAlpha, 1); + // filter_resize centers an 8x8 picture in 16x16, so (4,4) is picture and (0,0) is padding. + QCOMPARE(alphaAt(&transparent, 4, 4), 255); + QCOMPARE(alphaAt(&transparent, 0, 0), 0); + QCOMPARE(alphaAt(&transparent, 15, 15), 0); + pass("resizeAlphaMask explicit0"); + + CopiedImage opaque = runResize(1, 255); + QCOMPARE(opaque.width, 16); + QCOMPARE(opaque.height, 16); + QCOMPARE(opaque.hasAlpha, 0); + pass("resizeAlphaMask explicit255"); + } + + void resizeAlphaRoundTrip() + { + Profile profile; + + // Consumer-hint copy: a missing outer property clears a pre-existing inner 0. + { + mlt_frame outer = mlt_frame_init(NULL); + mlt_frame inner = mlt_frame_init(NULL); + AlphaCapture capture = {0, -1, -1}; + YuvSource spec = {8, 8, 40, 0, &capture}; + mlt_properties_set_int(MLT_FRAME_PROPERTIES(inner), "resize_alpha", 0); + attachSource(inner, &spec); + QCOMPARE(mlt_frame_prepend_image_from_service(outer, inner), 0); + CopiedImage image = fetch(outer, 8, 8); + QCOMPARE(image.width, 8); + QCOMPARE(capture.did, 1); + QCOMPARE(capture.exists, 0); + QCOMPARE(mlt_properties_exists(MLT_FRAME_PROPERTIES(outer), "resize_alpha"), 0); + QCOMPARE(mlt_properties_exists(MLT_FRAME_PROPERTIES(inner), "resize_alpha"), 0); + mlt_frame_close(outer); + mlt_frame_close(inner); + } + pass("resizeAlphaRoundTrip hint-missing"); + + // Consumer-hint copy: an explicit 0 stays an explicit 0. + { + mlt_frame outer = mlt_frame_init(NULL); + mlt_frame inner = mlt_frame_init(NULL); + AlphaCapture capture = {0, -1, -1}; + YuvSource spec = {8, 8, 40, 0, &capture}; + mlt_properties_set_int(MLT_FRAME_PROPERTIES(outer), "resize_alpha", 0); + mlt_properties_set_int(MLT_FRAME_PROPERTIES(inner), "resize_alpha", 255); + attachSource(inner, &spec); + QCOMPARE(mlt_frame_prepend_image_from_service(outer, inner), 0); + CopiedImage image = fetch(outer, 8, 8); + QCOMPARE(image.width, 8); + QCOMPARE(capture.did, 1); + QCOMPARE(capture.exists, 1); + QCOMPARE(capture.value, 0); + QCOMPARE(mlt_properties_get_int(MLT_FRAME_PROPERTIES(inner), "resize_alpha"), 0); + QCOMPARE(mlt_properties_exists(MLT_FRAME_PROPERTIES(inner), "resize_alpha"), 1); + mlt_frame_close(outer); + mlt_frame_close(inner); + } + pass("resizeAlphaRoundTrip hint-explicit0"); + + // Composite overwrites resize_alpha with 255 while unaligned, then restores it. + mlt_transition composite = mlt_factory_transition(profile.get_profile(), "composite", NULL); + QVERIFY(composite); + mlt_properties properties = MLT_TRANSITION_PROPERTIES(composite); + mlt_properties_set(properties, "geometry", "0/0:100%x100%:100"); + mlt_properties_set_int(properties, "aligned", 0); + mlt_properties_set_int(properties, "progressive", 1); + mlt_properties_set_int(properties, "in", 0); + mlt_properties_set_int(properties, "out", 10); + + { + mlt_frame a = mlt_frame_init(NULL); + mlt_frame b = mlt_frame_init(NULL); + AlphaCapture capture = {0, -1, -1}; + YuvSource dest = {16, 16, 40, 0, NULL}; + YuvSource src = {16, 16, 80, 0, &capture}; + attachSource(a, &dest); + attachSource(b, &src); + mlt_transition_process(composite, a, b); + CopiedImage image = fetch(a, 16, 16); + QCOMPARE(image.width, 16); + QCOMPARE(capture.did, 1); + QCOMPARE(capture.exists, 1); + QCOMPARE(capture.value, 255); + QCOMPARE(mlt_properties_exists(MLT_FRAME_PROPERTIES(b), "resize_alpha"), 0); + mlt_frame_close(b); + mlt_frame_close(a); + } + pass("resizeAlphaRoundTrip composite-missing"); + + { + mlt_frame a = mlt_frame_init(NULL); + mlt_frame b = mlt_frame_init(NULL); + AlphaCapture capture = {0, -1, -1}; + YuvSource dest = {16, 16, 40, 0, NULL}; + YuvSource src = {16, 16, 80, 0, &capture}; + mlt_properties_set_int(MLT_FRAME_PROPERTIES(b), "resize_alpha", 0); + attachSource(a, &dest); + attachSource(b, &src); + mlt_transition_process(composite, a, b); + CopiedImage image = fetch(a, 16, 16); + QCOMPARE(image.width, 16); + QCOMPARE(capture.did, 1); + QCOMPARE(capture.exists, 1); + QCOMPARE(capture.value, 255); + QCOMPARE(mlt_properties_exists(MLT_FRAME_PROPERTIES(b), "resize_alpha"), 1); + QCOMPARE(mlt_properties_get_int(MLT_FRAME_PROPERTIES(b), "resize_alpha"), 0); + mlt_frame_close(b); + mlt_frame_close(a); + } + pass("resizeAlphaRoundTrip composite-explicit0"); + mlt_transition_close(composite); + } + + void lumaEqualSize() + { + YuvSource dest = {16, 16, 40, 0, NULL}; + YuvSource src = {16, 16, 200, 0, NULL}; + + CopiedImage dissolve = runLuma(&dest, &src, 16, 16, 0); + QCOMPARE(dissolve.width, 16); + QCOMPARE(dissolve.height, 16); + QCOMPARE(dissolve.hasAlpha, 0); + QCOMPARE(yAt(&dissolve, 0, 0), halfMix(40, 200)); + QCOMPARE(yAt(&dissolve, 15, 15), halfMix(40, 200)); + pass("lumaEqualSize dissolve"); + + CopiedImage wipe = runLuma(&dest, &src, 16, 16, 1); + QCOMPARE(wipe.width, 16); + QCOMPARE(wipe.height, 16); + QCOMPARE(wipe.hasAlpha, 0); + QCOMPARE(yAt(&wipe, 0, 0), 200); + QCOMPARE(yAt(&wipe, 15, 15), 200); + pass("lumaEqualSize wipe"); + } + + void lumaMismatch() + { + // Dest 16x8 constant 40. Src 8x4, each row a distinct value. Overlap is 8x4. + YuvSource dest = {16, 8, 40, 0, NULL}; + YuvSource src = {8, 4, 100, 1, NULL}; + + CopiedImage dissolve = runLuma(&dest, &src, 16, 8, 0); + QCOMPARE(dissolve.width, 16); + QCOMPARE(dissolve.height, 8); + for (int y = 0; y < 4; y++) { + const int srcValue = 100 + y * 10; + QCOMPARE(yAt(&dissolve, 0, y), halfMix(40, srcValue)); + QCOMPARE(yAt(&dissolve, 7, y), halfMix(40, srcValue)); + QCOMPARE(yAt(&dissolve, 8, y), 40); + QCOMPARE(yAt(&dissolve, 15, y), 40); + } + for (int y = 4; y < 8; y++) { + QCOMPARE(yAt(&dissolve, 0, y), 40); + QCOMPARE(yAt(&dissolve, 15, y), 40); + } + pass("lumaMismatch dissolve"); + + CopiedImage wipe = runLuma(&dest, &src, 16, 8, 1); + QCOMPARE(wipe.width, 16); + QCOMPARE(wipe.height, 8); + for (int y = 0; y < 4; y++) { + const int srcValue = 100 + y * 10; + QCOMPARE(yAt(&wipe, 0, y), srcValue); + QCOMPARE(yAt(&wipe, 7, y), srcValue); + QCOMPARE(yAt(&wipe, 8, y), 40); + QCOMPARE(yAt(&wipe, 15, y), 40); + } + for (int y = 4; y < 8; y++) { + QCOMPARE(yAt(&wipe, 0, y), 40); + QCOMPARE(yAt(&wipe, 15, y), 40); + } + pass("lumaMismatch wipe"); + } + + void concealForward() + { + avformat_conceal state; + avformat_conceal_init(&state); + QCOMPARE(state.owned, (mlt_frame) NULL); + QCOMPARE(state.source_position, 0); + + mlt_cache cache = mlt_cache_init(); + QVERIFY(cache); + mlt_cache_set_size(cache, 4); + mlt_frame frame = makePicture(0x11, 5); + mlt_cache_put_frame_image(cache, frame); + avformat_conceal_record(&state, frame, cache, 10, 5); + + QCOMPARE(state.owned, (mlt_frame) NULL); + QCOMPARE((int) state.cache_position, 5); + mlt_frame fetched = avformat_conceal_fetch(&state, cache); + QVERIFY(fetched); + QCOMPARE(pictureMarker(fetched), 0x11); + QCOMPARE(state.owned, (mlt_frame) NULL); + pass("concealForward"); + + mlt_frame_close(fetched); + mlt_frame_close(frame); + mlt_cache_close(cache); + avformat_conceal_close(&state); + } + + void concealDetach() + { + avformat_conceal state; + avformat_conceal_init(&state); + mlt_cache cache = mlt_cache_init(); + QVERIFY(cache); + mlt_cache_set_size(cache, 2); + + mlt_frame good = makePicture(0x21, 3); + mlt_cache_put_frame_image(cache, good); + avformat_conceal_record(&state, good, cache, 10, 3); + QCOMPARE(state.owned, (mlt_frame) NULL); + + avformat_conceal_detach(&state, cache); + QVERIFY(state.owned); + QCOMPARE(pictureMarker(state.owned), 0x21); + + mlt_frame otherA = makePicture(0x22, 4); + mlt_frame otherB = makePicture(0x23, 5); + mlt_cache_put_frame_image(cache, otherA); + mlt_cache_put_frame_image(cache, otherB); + mlt_frame dropped = mlt_cache_get_frame(cache, 3); + QCOMPARE(dropped, (mlt_frame) NULL); + + mlt_frame fetched = avformat_conceal_fetch(&state, cache); + QVERIFY(fetched); + QCOMPARE(pictureMarker(fetched), 0x21); + pass("concealDetach"); + + mlt_frame_close(fetched); + mlt_frame_close(otherB); + mlt_frame_close(otherA); + mlt_frame_close(good); + mlt_cache_close(cache); + avformat_conceal_close(&state); + } + + void concealBackward() + { + avformat_conceal state; + avformat_conceal_init(&state); + mlt_cache cache = mlt_cache_init(); + QVERIFY(cache); + mlt_cache_set_size(cache, 4); + + mlt_frame newer = makePicture(0x31, 6); + mlt_cache_put_frame_image(cache, newer); + avformat_conceal_record(&state, newer, cache, 10, 6); + QCOMPARE(state.owned, (mlt_frame) NULL); + + mlt_frame older = makePicture(0x32, 7); + mlt_cache_put_frame_image(cache, older); + avformat_conceal_record(&state, older, cache, 4, 7); + + QCOMPARE(state.owned, (mlt_frame) NULL); + QCOMPARE((int) state.cache_position, 6); + QCOMPARE(state.source_position, 10); + mlt_frame fetched = avformat_conceal_fetch(&state, cache); + QVERIFY(fetched); + QCOMPARE(pictureMarker(fetched), 0x31); + pass("concealBackward"); + + mlt_frame_close(fetched); + mlt_frame_close(older); + mlt_frame_close(newer); + mlt_cache_close(cache); + avformat_conceal_close(&state); + } +}; + +QTEST_APPLESS_MAIN(TestDissolve) + +#include "test_dissolve.moc"