diff --git a/src/framework/mlt.vers b/src/framework/mlt.vers index b0ee6cb85..0c46a13b6 100644 --- a/src/framework/mlt.vers +++ b/src/framework/mlt.vers @@ -705,3 +705,8 @@ MLT_7.42.0 { mlt_frame_push_image_with_fx_cut; mlt_producer_pass_frame_properties; } MLT_7.40.0; + +MLT_7.44.0 { + global: + mlt_properties_revision; +} MLT_7.42.0; diff --git a/src/framework/mlt_playlist.c b/src/framework/mlt_playlist.c index 795e711fd..967a2bde1 100644 --- a/src/framework/mlt_playlist.c +++ b/src/framework/mlt_playlist.c @@ -46,8 +46,54 @@ struct playlist_entry_s mlt_position producer_length; mlt_event event; int preservation_hack; + unsigned int revision; + int is_blank; }; +/** Determine if a playlist entry is a blank. + * + * Whether the producer of an entry is a blank does not change, so it is + * determined once when the entry is created. An entry whose producer has been + * closed by autoclose counts as a blank, as mlt_producer_is_blank() does for NULL. + * \private \memberof mlt_playlist_s + * \param entry a playlist entry + * \return true if the entry is a blank + */ + +static inline int entry_is_blank(playlist_entry *entry) +{ + return entry->producer == NULL || entry->is_blank; +} + +/** Determine if the producer of a playlist entry may have changed. + * + * The producer's points are only read again when one of its properties has + * changed since the last time this was checked for the entry. + * \private \memberof mlt_playlist_s + * \param entry a playlist entry with a producer + * \return true if the producer may have changed + */ + +static int entry_producer_changed(playlist_entry *entry) +{ + unsigned int revision = mlt_properties_revision(MLT_PRODUCER_PROPERTIES(entry->producer)); + int changed = revision != entry->revision; + entry->revision = revision; + return changed; +} + +/** Make the next refresh compare an entry with the points of its producer. + * + * \private \memberof mlt_playlist_s + * \param entry a playlist entry + */ + +static void entry_force_refresh(playlist_entry *entry) +{ + if (entry->producer) + entry->revision = mlt_properties_revision(MLT_PRODUCER_PROPERTIES(entry->producer)) - 1; +} + /* Forward declarations */ @@ -187,7 +233,7 @@ static int mlt_playlist_virtual_refresh(mlt_playlist self) for (i = 0; i < self->count; i++) { // Get the producer mlt_producer producer = self->list[i]->producer; - if (producer) { + if (producer && entry_producer_changed(self->list[i])) { int current_length = mlt_producer_get_playtime(producer); // Check if the length of the producer has changed @@ -257,9 +303,10 @@ static int mlt_playlist_virtual_append(mlt_playlist self, mlt_producer producer = NULL; mlt_properties properties = NULL; mlt_properties parent = NULL; + int is_blank = mlt_producer_is_blank(source); // If we have a cut, then use the in/out points from the cut - if (mlt_producer_is_blank(source)) { + if (is_blank) { mlt_position length = out - in + 1; mlt_producer blank = blank_producer(self); @@ -334,7 +381,9 @@ static int mlt_playlist_virtual_append(mlt_playlist self, self->list[self->count]->frame_out = out; self->list[self->count]->frame_count = out - in + 1; self->list[self->count]->repeat = 1; + self->list[self->count]->is_blank = is_blank; self->list[self->count]->producer_length = mlt_producer_get_playtime(producer); + entry_force_refresh(self->list[self->count]); self->list[self->count]->event = mlt_events_listen(parent, self, "producer-changed", @@ -519,6 +568,7 @@ static mlt_producer mlt_playlist_virtual_set_out(mlt_playlist self) // Update the frame_count for the changed clip (hmmm) self->list[i]->frame_out = position; self->list[i]->frame_count = self->list[i]->frame_out - self->list[i]->frame_in + 1; + entry_force_refresh(self->list[i]); // Refresh the playlist mlt_playlist_virtual_refresh(self); @@ -1707,7 +1757,7 @@ void mlt_playlist_consolidate_blanks(mlt_playlist self, int keep_length) playlist_entry *left = self->list[i - 1]; playlist_entry *right = self->list[i]; - if (mlt_producer_is_blank(left->producer) && mlt_producer_is_blank(right->producer)) { + if (entry_is_blank(left) && entry_is_blank(right)) { mlt_playlist_resize_clip(self, i - 1, 0, left->frame_count + right->frame_count - 1); mlt_playlist_remove(self, i--); } @@ -1715,7 +1765,7 @@ void mlt_playlist_consolidate_blanks(mlt_playlist self, int keep_length) if (!keep_length && self->count > 0) { playlist_entry *last = self->list[self->count - 1]; - if (mlt_producer_is_blank(last->producer)) + if (entry_is_blank(last)) mlt_playlist_remove(self, self->count - 1); } @@ -1734,7 +1784,7 @@ void mlt_playlist_consolidate_blanks(mlt_playlist self, int keep_length) int mlt_playlist_is_blank(mlt_playlist self, int clip) { - return self == NULL || mlt_producer_is_blank(mlt_playlist_get_clip(self, clip)); + return self == NULL || clip < 0 || clip >= self->count || entry_is_blank(self->list[clip]); } /** Determine if the specified position is a blank. @@ -1747,7 +1797,9 @@ int mlt_playlist_is_blank(mlt_playlist self, int clip) int mlt_playlist_is_blank_at(mlt_playlist self, mlt_position position) { - return self == NULL || mlt_producer_is_blank(mlt_playlist_get_clip_at(self, position)); + int index = 0, total = 0; + return self == NULL || mlt_playlist_locate(self, &position, &index, &total) == NULL + || entry_is_blank(self->list[index]); } /** Replace the specified clip with a blank and return the clip. diff --git a/src/framework/mlt_properties.c b/src/framework/mlt_properties.c index d0b722f80..0ae2400b8 100644 --- a/src/framework/mlt_properties.c +++ b/src/framework/mlt_properties.c @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -66,6 +67,7 @@ typedef struct mlt_properties *children_properties; char **children_names; int children_count; + atomic_uint revision; } property_list; /* Memory leak checks */ @@ -485,6 +487,26 @@ int mlt_properties_ref_count(mlt_properties self) return 0; } +/** Get the revision of a properties list. + * + * The revision changes whenever a property is set, cleared or renamed, so + * comparing it with a previously saved value tells whether any property may + * have changed since then. The value itself has no meaning and wraps around. + * + * \public \memberof mlt_properties_s + * \param self a properties list + * \return the current revision + */ + +unsigned int mlt_properties_revision(mlt_properties self) +{ + if (self != NULL) { + property_list *list = self->local; + return atomic_load(&list->revision); + } + return 0; +} + /** Set a properties list to be a mirror copy of another. * * Note that this does not copy all existing properties. Rather, you must @@ -736,6 +758,7 @@ static mlt_property mlt_properties_fetch(mlt_properties self, const char *name) static void fire_property_changed(mlt_properties self, const char *name) { + atomic_fetch_add(&((property_list *) self->local)->revision, 1); mlt_events_fire(self, "property-changed", mlt_event_data_from_string(name)); } @@ -1419,6 +1442,8 @@ int mlt_properties_rename(mlt_properties self, const char *source, const char *d } } } + if (found) + atomic_fetch_add(&list->revision, 1); mlt_properties_unlock(self); } return value != NULL; diff --git a/src/framework/mlt_properties.h b/src/framework/mlt_properties.h index 18ab88691..dde61feee 100644 --- a/src/framework/mlt_properties.h +++ b/src/framework/mlt_properties.h @@ -3,7 +3,7 @@ * \brief Properties class declaration * \see mlt_properties_s * - * Copyright (C) 2003-2022 Meltytech, LLC + * Copyright (C) 2003-2026 Meltytech, LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -56,6 +56,7 @@ MLT_EXPORT int mlt_properties_preset(mlt_properties self, const char *name); MLT_EXPORT int mlt_properties_inc_ref(mlt_properties self); MLT_EXPORT int mlt_properties_dec_ref(mlt_properties self); MLT_EXPORT int mlt_properties_ref_count(mlt_properties self); +MLT_EXPORT unsigned int mlt_properties_revision(mlt_properties self); MLT_EXPORT void mlt_properties_mirror(mlt_properties self, mlt_properties that); MLT_EXPORT int mlt_properties_inherit(mlt_properties self, mlt_properties that); MLT_EXPORT int mlt_properties_copy(mlt_properties self, mlt_properties that, const char *prefix); diff --git a/src/tests/test_playlist/test_playlist.cpp b/src/tests/test_playlist/test_playlist.cpp index 7baf8213c..637d1940d 100644 --- a/src/tests/test_playlist/test_playlist.cpp +++ b/src/tests/test_playlist/test_playlist.cpp @@ -284,6 +284,97 @@ private Q_SLOTS: QCOMPARE(frame->get_int("consumer.dummy"), 1); delete frame; } + + void RefreshDetectsCutChange() + { + Producer p(profile, "color", "red"); + p.set("length", 100); + p.set_in_and_out(0, 99); + Playlist pl(profile); + for (int i = 0; i < 4; i++) + pl.append(p, 0, 9); + QCOMPARE(pl.get_playtime(), 40); + + // A cut does not notify the playlist when its points change. + Producer *cut = pl.get_clip(0); + QVERIFY(cut != nullptr); + cut->set_in_and_out(0, 4); + delete cut; + QCOMPARE(pl.clip_length(0), 10); + + // The next edit refreshes the playlist from the producers. + pl.blank(9); + QCOMPARE(pl.clip_length(0), 5); + QCOMPARE(pl.clip_start(1), 5); + QCOMPARE(pl.get_playtime(), 45); + + cut = pl.get_clip(2); + QVERIFY(cut != nullptr); + cut->set_in_and_out(2, 4); + delete cut; + QCOMPARE(pl.resize_clip(1, 0, 19), 0); + QCOMPARE(pl.clip_length(1), 20); + QCOMPARE(pl.clip_length(2), 3); + QCOMPARE(pl.clip_start(3), 28); + QCOMPARE(pl.get_playtime(), 48); + + // So does a change of the parent producer. + cut = pl.get_clip(3); + QVERIFY(cut != nullptr); + cut->set_in_and_out(0, 0); + delete cut; + p.set("out", 98); + QCOMPARE(pl.clip_length(3), 1); + QCOMPARE(pl.get_playtime(), 39); + } + + void BlankStatus() + { + Producer p(profile, "color", "red"); + p.set("length", 100); + p.set_in_and_out(0, 99); + Playlist pl(profile); + pl.blank(4); + pl.append(p, 0, 9); + pl.blank(4); + pl.blank(2); + pl.append(p, 0, 9); + pl.blank(6); + QCOMPARE(pl.count(), 6); + QVERIFY(pl.is_blank(0)); + QVERIFY(!pl.is_blank(1)); + QVERIFY(pl.is_blank(2)); + QVERIFY(pl.is_blank(3)); + QVERIFY(!pl.is_blank(4)); + QVERIFY(pl.is_blank(5)); + QVERIFY(pl.is_blank(-1)); + QVERIFY(pl.is_blank(6)); + QVERIFY(pl.is_blank_at(0)); + QVERIFY(!pl.is_blank_at(5)); + QVERIFY(pl.is_blank_at(15)); + QVERIFY(pl.is_blank_at(1000)); + + pl.consolidate_blanks(0); + QCOMPARE(pl.count(), 4); + QVERIFY(pl.is_blank(0)); + QVERIFY(!pl.is_blank(1)); + QVERIFY(pl.is_blank(2)); + QCOMPARE(pl.clip_length(2), 8); + QVERIFY(!pl.is_blank(3)); + QCOMPARE(pl.get_playtime(), 33); + + // A clip replaced by a blank is merged with its neighbours. + Producer *clip = pl.replace_with_blank(3); + QVERIFY(clip != nullptr); + delete clip; + QVERIFY(pl.is_blank(3)); + pl.consolidate_blanks(1); + QCOMPARE(pl.count(), 3); + QCOMPARE(pl.clip_length(2), 18); + pl.consolidate_blanks(0); + QCOMPARE(pl.count(), 2); + QCOMPARE(pl.get_playtime(), 15); + } }; QTEST_APPLESS_MAIN(TestPlaylist) diff --git a/src/tests/test_properties/test_properties.cpp b/src/tests/test_properties/test_properties.cpp index a64dadc4c..d9ef7f6ed 100644 --- a/src/tests/test_properties/test_properties.cpp +++ b/src/tests/test_properties/test_properties.cpp @@ -1583,6 +1583,45 @@ private Q_SLOTS: QCOMPARE(p.get_int("foo"), 123); QCOMPARE(p.get_double("foo"), 123.4); } + + void RevisionChangesOnModification() + { + Properties p; + mlt_properties props = p.get_properties(); + unsigned int revision = mlt_properties_revision(props); + p.set("foo", "bar"); + QVERIFY(mlt_properties_revision(props) != revision); + revision = mlt_properties_revision(props); + p.set("foo", 10); + QVERIFY(mlt_properties_revision(props) != revision); + revision = mlt_properties_revision(props); + p.anim_set("anim", 1.0, 5); + QVERIFY(mlt_properties_revision(props) != revision); + revision = mlt_properties_revision(props); + p.set("data", &revision, 0); + QVERIFY(mlt_properties_revision(props) != revision); + revision = mlt_properties_revision(props); + p.clear("foo"); + QVERIFY(mlt_properties_revision(props) != revision); + revision = mlt_properties_revision(props); + QCOMPARE(mlt_properties_rename(props, "anim", "renamed"), 0); + QVERIFY(mlt_properties_revision(props) != revision); + } + + void RevisionUnchangedOnRead() + { + Properties p; + p.set("foo", "123"); + p.anim_set("anim", 1.0, 5); + mlt_properties props = p.get_properties(); + const unsigned int revision = mlt_properties_revision(props); + QCOMPARE(p.get_int("foo"), 123); + QCOMPARE(p.get("foo"), "123"); + QCOMPARE(p.anim_get_double("anim", 5), 1.0); + QCOMPARE(p.get("missing"), nullptr); + QCOMPARE(mlt_properties_rename(props, "missing", "other"), 0); + QCOMPARE(mlt_properties_revision(props), revision); + } }; QTEST_APPLESS_MAIN(TestProperties)