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); 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 f1cc5209c..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,8 +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 + avformat_conceal conceal; // last good picture for video error concealment AVFilterGraph *vfilter_graph; AVFilterContext *vfilter_in; AVFilterContext *vfilter_out; @@ -230,6 +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)); + avformat_conceal_init(&self->conceal); producer = calloc(1, sizeof(struct mlt_producer_s)); // Initialise it @@ -2801,6 +2802,7 @@ static int producer_get_image(mlt_frame frame, if (self->reset_image_cache) { self->reset_image_cache = 0; + avformat_conceal_detach(&self->conceal, self->image_cache); mlt_cache_close(self->image_cache); self->image_cache = NULL; av_frame_free(&self->video_frame); @@ -3033,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 @@ -3240,6 +3242,8 @@ 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. + 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)) { @@ -3251,33 +3255,34 @@ static int producer_get_image(mlt_frame frame, mlt_cache_put_frame_image(self->image_cache, frame); } } - // Clone frame for error concealment. - 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); - } - } else if (self->last_good_frame) { + // For error concealment; avoid a second copy when the image cache already has one. + 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 = 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 = avformat_conceal_fetch(&self->conceal, self->image_cache); + 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 +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; + 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. @@ -4654,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) { diff --git a/src/modules/core/filter_resize.c b/src/modules/core/filter_resize.c index 1fffebd8a..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,6 +178,14 @@ 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 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); + 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_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 c8eb438be..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,39 +95,113 @@ 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; } +// 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; +} + +// 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); +} + +/* 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) +{ + mlt_image_format format = mlt_image_yuv422; + + if (fix_background_alpha) { + request_transparent_padding(dst_frame); + request_transparent_padding(src_frame); + } + + 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)); + + // 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); + } +} + static inline int dissolve_yuv422(mlt_frame frame, mlt_frame that, float weight, @@ -138,53 +212,39 @@ 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 = (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 is_translucent = 0; int mix = weight * (1 << 16); - 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; - 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)); - - // 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 { - 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; - } - } + 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; } @@ -292,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) { @@ -322,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, @@ -341,40 +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 = (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; uint8_t *p_src, *p_dest; uint8_t *alpha_src, *alpha_dest; int i, j; int stride_src; int stride_dest; + 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); - 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); + + 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)); - - // 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; @@ -407,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--) { @@ -462,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; @@ -1024,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 @@ -1066,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) { @@ -1083,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, 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"