1
0
Эх сурвалжийг харах

UI: Add visibility transitions

This also modifies libobs.

This adds the ability for scene items to have transitions
when their visibility is toggled.
Exeldro 5 жил өмнө
parent
commit
32e7ba1abe

+ 2 - 0
UI/data/locale/en-US.ini

@@ -80,6 +80,8 @@ Back="Back"
 Defaults="Defaults"
 HideMixer="Hide in Mixer"
 TransitionOverride="Transition Override"
+ShowTransition="Show Transition"
+HideTransition="Hide Transition"
 None="None"
 StudioMode.Preview="Preview"
 StudioMode.Program="Program"

+ 162 - 0
UI/window-basic-main-transitions.cpp

@@ -1102,6 +1102,168 @@ QMenu *OBSBasic::CreatePerSceneTransitionMenu()
 	return menu;
 }
 
+void OBSBasic::on_actionShowTransitionProperties_triggered()
+{
+	OBSSceneItem item = GetCurrentSceneItem();
+	OBSSource source = obs_sceneitem_get_show_transition(item);
+
+	if (source)
+		CreatePropertiesWindow(source);
+}
+
+void OBSBasic::on_actionHideTransitionProperties_triggered()
+{
+	OBSSceneItem item = GetCurrentSceneItem();
+	OBSSource source = obs_sceneitem_get_hide_transition(item);
+
+	if (source)
+		CreatePropertiesWindow(source);
+}
+
+QMenu *OBSBasic::CreateVisibilityTransitionMenu(bool visible)
+{
+	OBSSceneItem si = GetCurrentSceneItem();
+
+	QMenu *menu =
+		new QMenu(QTStr(visible ? "ShowTransition" : "HideTransition"));
+	QAction *action;
+
+	const char *curId = obs_source_get_id(
+		visible ? obs_sceneitem_get_show_transition(si)
+			: obs_sceneitem_get_hide_transition(si));
+	int curDuration =
+		(int)(visible ? obs_sceneitem_get_show_transition_duration(si)
+			      : obs_sceneitem_get_hide_transition_duration(si));
+
+	if (curDuration <= 0)
+		curDuration = obs_frontend_get_transition_duration();
+
+	QSpinBox *duration = new QSpinBox(menu);
+	duration->setMinimum(50);
+	duration->setSuffix("ms");
+	duration->setMaximum(20000);
+	duration->setSingleStep(50);
+	duration->setValue(curDuration);
+
+	auto setTransition = [this](QAction *action, bool visible) {
+		OBSBasic *main =
+			reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+
+		QString id = action->property("transition_id").toString();
+		OBSSceneItem sceneItem = main->GetCurrentSceneItem();
+
+		if (id.isNull() || id.isEmpty()) {
+			if (visible)
+				obs_sceneitem_set_show_transition(sceneItem,
+								  nullptr);
+			else
+				obs_sceneitem_set_hide_transition(sceneItem,
+								  nullptr);
+		} else {
+			OBSSource tr =
+				visible ? obs_sceneitem_get_show_transition(
+						  sceneItem)
+					: obs_sceneitem_get_hide_transition(
+						  sceneItem);
+
+			if (!tr || strcmp(QT_TO_UTF8(id),
+					  obs_source_get_id(tr)) != 0) {
+				QString name = QT_UTF8(obs_source_get_name(
+					obs_sceneitem_get_source(sceneItem)));
+				name += " ";
+				name += QTStr(visible ? "ShowTransition"
+						      : "HideTransition");
+				tr = obs_source_create_private(QT_TO_UTF8(id),
+							       QT_TO_UTF8(name),
+							       nullptr);
+				if (visible)
+					obs_sceneitem_set_show_transition(
+						sceneItem, tr);
+				else
+					obs_sceneitem_set_hide_transition(
+						sceneItem, tr);
+				obs_source_release(tr);
+
+				int duration =
+					(int)(visible ? obs_sceneitem_get_show_transition_duration(
+								sceneItem)
+						      : obs_sceneitem_get_hide_transition_duration(
+								sceneItem));
+				if (duration <= 0) {
+					duration =
+						obs_frontend_get_transition_duration();
+					if (visible)
+						obs_sceneitem_set_show_transition_duration(
+							sceneItem, duration);
+					else
+						obs_sceneitem_set_hide_transition_duration(
+							sceneItem, duration);
+				}
+			}
+			if (obs_source_configurable(tr))
+				CreatePropertiesWindow(tr);
+		}
+	};
+	if (visible) {
+		auto setDuration = [this](int duration) {
+			OBSBasic *main = reinterpret_cast<OBSBasic *>(
+				App()->GetMainWindow());
+
+			OBSSceneItem item = main->GetCurrentSceneItem();
+			obs_sceneitem_set_show_transition_duration(item,
+								   duration);
+		};
+		connect(duration,
+			(void (QSpinBox::*)(int)) & QSpinBox::valueChanged,
+			setDuration);
+	} else {
+		auto setDuration = [this](int duration) {
+			OBSBasic *main = reinterpret_cast<OBSBasic *>(
+				App()->GetMainWindow());
+
+			OBSSceneItem item = main->GetCurrentSceneItem();
+			obs_sceneitem_set_hide_transition_duration(item,
+								   duration);
+		};
+		connect(duration,
+			(void (QSpinBox::*)(int)) & QSpinBox::valueChanged,
+			setDuration);
+	}
+
+	action = menu->addAction(QT_UTF8(Str("None")));
+	action->setProperty("transition_id", QT_UTF8(""));
+	action->setCheckable(true);
+	action->setChecked(!curId);
+	connect(action, &QAction::triggered,
+		std::bind(setTransition, action, visible));
+	size_t idx = 0;
+	const char *id;
+	while (obs_enum_transition_types(idx++, &id)) {
+		const char *name = obs_source_get_display_name(id);
+		const bool match = id && curId && strcmp(id, curId) == 0;
+		action = menu->addAction(QT_UTF8(name));
+		action->setProperty("transition_id", QT_UTF8(id));
+		action->setCheckable(true);
+		action->setChecked(match);
+		connect(action, &QAction::triggered,
+			std::bind(setTransition, action, visible));
+	}
+
+	QWidgetAction *durationAction = new QWidgetAction(menu);
+	durationAction->setDefaultWidget(duration);
+
+	menu->addSeparator();
+	menu->addAction(durationAction);
+	if (curId && obs_is_source_configurable(curId)) {
+		menu->addSeparator();
+		menu->addAction(
+			QTStr("Properties"), this,
+			visible ? SLOT(on_actionShowTransitionProperties_triggered())
+				: SLOT(on_actionHideTransitionProperties_triggered()));
+	}
+	return menu;
+}
+
 QMenu *OBSBasic::CreateTransitionMenu(QWidget *parent, QuickTransition *qt)
 {
 	QMenu *menu = new QMenu(parent);

+ 4 - 0
UI/window-basic-main.cpp

@@ -5065,6 +5065,10 @@ void OBSBasic::CreateSourcePopupMenu(int idx, bool preview)
 				SLOT(ScreenshotSelectedSource()));
 		popup.addSeparator();
 
+		popup.addMenu(CreateVisibilityTransitionMenu(true));
+		popup.addMenu(CreateVisibilityTransitionMenu(false));
+		popup.addSeparator();
+
 		action = popup.addAction(QTStr("Interact"), this,
 					 SLOT(on_actionInteract_triggered()));
 

+ 4 - 0
UI/window-basic-main.hpp

@@ -417,6 +417,7 @@ private:
 	void CreateDefaultQuickTransitions();
 
 	QMenu *CreatePerSceneTransitionMenu();
+	QMenu *CreateVisibilityTransitionMenu(bool visible);
 
 	QuickTransition *GetQuickTransition(int id);
 	int GetQuickTransitionIdx(int id);
@@ -958,6 +959,9 @@ private slots:
 	void on_transitionDuration_valueChanged(int value);
 	void on_tbar_position_valueChanged(int value);
 
+	void on_actionShowTransitionProperties_triggered();
+	void on_actionHideTransitionProperties_triggered();
+
 	void on_modeSwitch_clicked();
 
 	// Source Context Buttons

+ 3 - 3
UI/window-basic-properties.cpp

@@ -408,11 +408,11 @@ void OBSBasicProperties::DrawTransitionPreview(void *data, uint32_t cx,
 {
 	OBSBasicProperties *window = static_cast<OBSBasicProperties *>(data);
 
-	if (!window->source)
+	if (!window->sourceClone)
 		return;
 
-	uint32_t sourceCX = max(obs_source_get_width(window->source), 1u);
-	uint32_t sourceCY = max(obs_source_get_height(window->source), 1u);
+	uint32_t sourceCX = max(obs_source_get_width(window->sourceClone), 1u);
+	uint32_t sourceCY = max(obs_source_get_height(window->sourceClone), 1u);
 
 	int x, y;
 	int newCX, newCY;

+ 34 - 0
docs/sphinx/reference-scenes.rst

@@ -470,6 +470,40 @@ Scene Item Functions
 
 ---------------------
 
+.. function:: void obs_sceneitem_set_show_transition(obs_sceneitem_t *item, obs_source_t *transition)
+              void obs_sceneitem_set_hide_transition(obs_sceneitem_t *item, obs_source_t *transition)
+
+   Set a transition for showing or hiding a scene item. Set *NULL* to remove the transition.
+
+---------------------
+
+.. function:: obs_source_t *obs_sceneitem_get_show_transition(obs_sceneitem_t *item)
+              obs_source_t *obs_sceneitem_get_hide_transition(obs_sceneitem_t *item)
+
+   :return: The transition for showing or hiding a scene item. *NULL* if no transition is set.
+
+---------------------
+
+.. function:: void obs_sceneitem_set_show_transition_duration(obs_sceneitem_t *item, uint32_t duration_ms)
+              void obs_sceneitem_set_hide_transition_duration(obs_sceneitem_t *item, uint32_t duration_ms)
+
+   Set transition duration for showing or hiding a scene item.
+
+---------------------
+
+.. function:: uint32_t obs_sceneitem_get_show_transition_duration(obs_sceneitem_t *item)
+              uint32_t obs_sceneitem_get_hide_transition_duration(obs_sceneitem_t *item)
+
+   :return: The transition duration in ms for showing or hiding a scene item.
+
+---------------------
+
+.. function:: void obs_sceneitem_do_transition(obs_sceneitem_t *item, bool visible)
+
+   Start the transition for showing or hiding a scene item.
+
+---------------------
+
 
 .. _scene_item_group_reference:
 

+ 333 - 11
libobs/obs-scene.c

@@ -187,6 +187,12 @@ static void scene_destroy(void *data)
 	bfree(scene);
 }
 
+static inline bool transition_active(obs_source_t *transition)
+{
+	return transition && (transition->transitioning_audio ||
+			      transition->transitioning_video);
+}
+
 static void scene_enum_sources(void *data, obs_source_enum_proc_t enum_callback,
 			       void *param, bool active)
 {
@@ -201,8 +207,28 @@ static void scene_enum_sources(void *data, obs_source_enum_proc_t enum_callback,
 		next = item->next;
 
 		obs_sceneitem_addref(item);
-		if (!active || os_atomic_load_long(&item->active_refs) > 0)
+		if (active) {
+			if (item->visible &&
+			    transition_active(item->show_transition))
+				enum_callback(scene->source,
+					      item->show_transition, param);
+			else if (!item->visible &&
+				 transition_active(item->hide_transition))
+				enum_callback(scene->source,
+					      item->hide_transition, param);
+			else if (os_atomic_load_long(&item->active_refs) > 0)
+				enum_callback(scene->source, item->source,
+					      param);
+		} else {
+			if (item->show_transition)
+				enum_callback(scene->source,
+					      item->show_transition, param);
+			if (item->hide_transition)
+				enum_callback(scene->source,
+					      item->hide_transition, param);
 			enum_callback(scene->source, item->source, param);
+		}
+
 		obs_sceneitem_release(item);
 
 		item = next;
@@ -565,7 +591,27 @@ static inline void render_item(struct obs_scene_item *item)
 			gs_matrix_translate3f(-(float)item->crop.left,
 					      -(float)item->crop.top, 0.0f);
 
-			obs_source_video_render(item->source);
+			if (item->user_visible &&
+			    transition_active(item->show_transition)) {
+				const int cx =
+					obs_source_get_width(item->source);
+				const int cy =
+					obs_source_get_height(item->source);
+				obs_transition_set_size(item->show_transition,
+							cx, cy);
+				obs_source_video_render(item->show_transition);
+			} else if (!item->user_visible &&
+				   transition_active(item->hide_transition)) {
+				const int cx =
+					obs_source_get_width(item->source);
+				const int cy =
+					obs_source_get_height(item->source);
+				obs_transition_set_size(item->hide_transition,
+							cx, cy);
+				obs_source_video_render(item->hide_transition);
+			} else {
+				obs_source_video_render(item->source);
+			}
 
 			gs_texrender_end(item->item_render);
 		}
@@ -576,6 +622,18 @@ static inline void render_item(struct obs_scene_item *item)
 	gs_matrix_mul(&item->draw_transform);
 	if (item->item_render) {
 		render_item_texture(item);
+	} else if (item->user_visible &&
+		   transition_active(item->show_transition)) {
+		const int cx = obs_source_get_width(item->source);
+		const int cy = obs_source_get_height(item->source);
+		obs_transition_set_size(item->show_transition, cx, cy);
+		obs_source_video_render(item->show_transition);
+	} else if (!item->user_visible &&
+		   transition_active(item->hide_transition)) {
+		const int cx = obs_source_get_width(item->source);
+		const int cy = obs_source_get_height(item->source);
+		obs_transition_set_size(item->hide_transition, cx, cy);
+		obs_source_video_render(item->hide_transition);
 	} else {
 		obs_source_video_render(item->source);
 	}
@@ -669,7 +727,8 @@ static void scene_video_render(void *data, gs_effect_t *effect)
 
 	item = scene->first_item;
 	while (item) {
-		if (item->user_visible)
+		if (item->user_visible ||
+		    transition_active(item->hide_transition))
 			render_item(item);
 
 		item = item->next;
@@ -793,6 +852,40 @@ static void scene_load_item(struct obs_scene *scene, obs_data_t *item_data)
 			item->scale_filter = OBS_SCALE_AREA;
 	}
 
+	obs_data_t *show_data = obs_data_get_obj(item_data, "show_transition");
+	if (show_data) {
+		const char *id = obs_data_get_string(show_data, "id");
+		if (id && strlen(id)) {
+			const char *tn = obs_data_get_string(show_data, "name");
+			obs_data_t *s =
+				obs_data_get_obj(show_data, "transition");
+			obs_source_t *t = obs_source_create_private(id, tn, s);
+			obs_sceneitem_set_show_transition(item, t);
+			obs_source_release(t);
+			obs_data_release(s);
+		}
+		item->show_transition_duration =
+			obs_data_get_int(show_data, "duration");
+		obs_data_release(show_data);
+	}
+
+	obs_data_t *hide_data = obs_data_get_obj(item_data, "hide_transition");
+	if (hide_data) {
+		const char *id = obs_data_get_string(hide_data, "id");
+		if (id && strlen(id)) {
+			const char *tn = obs_data_get_string(hide_data, "name");
+			obs_data_t *s =
+				obs_data_get_obj(hide_data, "transition");
+			obs_source_t *t = obs_source_create_private(id, tn, s);
+			obs_sceneitem_set_hide_transition(item, t);
+			obs_source_release(t);
+			obs_data_release(s);
+		}
+		item->hide_transition_duration =
+			obs_data_get_int(hide_data, "duration");
+		obs_data_release(hide_data);
+	}
+
 	if (item->item_render && !item_texture_enabled(item)) {
 		obs_enter_graphics();
 		gs_texrender_destroy(item->item_render);
@@ -908,6 +1001,40 @@ static void scene_save_item(obs_data_array_t *array,
 
 	obs_data_set_string(item_data, "scale_filter", scale_filter);
 
+	obs_data_t *show_data = obs_data_create();
+	if (item->show_transition) {
+		obs_data_set_string(
+			show_data, "id",
+			obs_source_get_unversioned_id(item->show_transition));
+		obs_data_set_string(show_data, "versioned_id",
+				    obs_source_get_id(item->show_transition));
+		obs_data_set_string(show_data, "name",
+				    obs_source_get_name(item->show_transition));
+		obs_data_t *s = obs_source_get_settings(item->show_transition);
+		obs_data_set_obj(show_data, "transition", s);
+		obs_data_release(s);
+	}
+	obs_data_set_int(show_data, "duration", item->show_transition_duration);
+	obs_data_set_obj(item_data, "show_transition", show_data);
+	obs_data_release(show_data);
+
+	obs_data_t *hide_data = obs_data_create();
+	if (item->hide_transition) {
+		obs_data_set_string(
+			hide_data, "id",
+			obs_source_get_unversioned_id(item->hide_transition));
+		obs_data_set_string(hide_data, "versioned_id",
+				    obs_source_get_id(item->hide_transition));
+		obs_data_set_string(hide_data, "name",
+				    obs_source_get_name(item->hide_transition));
+		obs_data_t *s = obs_source_get_settings(item->hide_transition);
+		obs_data_set_obj(hide_data, "transition", s);
+		obs_data_release(s);
+	}
+	obs_data_set_int(hide_data, "duration", item->hide_transition_duration);
+	obs_data_set_obj(item_data, "hide_transition", hide_data);
+	obs_data_release(hide_data);
+
 	obs_data_set_obj(item_data, "private_settings", item->private_settings);
 
 	obs_data_array_push_back(array, item_data);
@@ -1079,9 +1206,19 @@ static bool scene_audio_render(void *data, uint64_t *ts_out,
 
 	item = scene->first_item;
 	while (item) {
-		if (!obs_source_audio_pending(item->source) && item->visible) {
+		struct obs_source *source;
+		if (item->visible && transition_active(item->show_transition))
+			source = item->show_transition;
+		else if (!item->visible &&
+			 transition_active(item->hide_transition))
+			source = item->hide_transition;
+		else
+			source = item->source;
+		if (!obs_source_audio_pending(source) &&
+		    (item->visible ||
+		     transition_active(item->hide_transition))) {
 			uint64_t source_ts =
-				obs_source_get_audio_timestamp(item->source);
+				obs_source_get_audio_timestamp(source);
 
 			if (source_ts && (!timestamp || source_ts < timestamp))
 				timestamp = source_ts;
@@ -1108,16 +1245,24 @@ static bool scene_audio_render(void *data, uint64_t *ts_out,
 		uint64_t source_ts;
 		size_t pos, count;
 		bool apply_buf;
+		struct obs_source *source;
+		if (item->visible && transition_active(item->show_transition))
+			source = item->show_transition;
+		else if (!item->visible &&
+			 transition_active(item->hide_transition))
+			source = item->hide_transition;
+		else
+			source = item->source;
 
 		apply_buf = apply_scene_item_volume(item, buf, timestamp,
 						    sample_rate);
 
-		if (obs_source_audio_pending(item->source)) {
+		if (obs_source_audio_pending(source)) {
 			item = item->next;
 			continue;
 		}
 
-		source_ts = obs_source_get_audio_timestamp(item->source);
+		source_ts = obs_source_get_audio_timestamp(source);
 		if (!source_ts) {
 			item = item->next;
 			continue;
@@ -1127,12 +1272,14 @@ static bool scene_audio_render(void *data, uint64_t *ts_out,
 						 source_ts - timestamp);
 		count = AUDIO_OUTPUT_FRAMES - pos;
 
-		if (!apply_buf && !item->visible) {
+		if (!apply_buf && !item->visible &&
+		    !transition_active(item->hide_transition)) {
 			item = item->next;
 			continue;
 		}
 
-		obs_source_get_audio_mix(item->source, &child_audio);
+		obs_source_get_audio_mix(source, &child_audio);
+
 		for (size_t mix = 0; mix < MAX_AUDIO_MIXES; mix++) {
 			if ((mixers & (1 << mix)) == 0)
 				continue;
@@ -1284,6 +1431,23 @@ static inline void duplicate_item_data(struct obs_scene_item *dst,
 	dst->bounds_align = src->bounds_align;
 	dst->bounds = src->bounds;
 
+	if (src->show_transition) {
+		obs_source_t *transition = obs_source_duplicate(
+			src->show_transition,
+			obs_source_get_name(src->show_transition), true);
+		obs_sceneitem_set_show_transition(dst, transition);
+		obs_source_release(transition);
+	}
+	if (src->hide_transition) {
+		obs_source_t *transition = obs_source_duplicate(
+			src->hide_transition,
+			obs_source_get_name(src->hide_transition), true);
+		obs_sceneitem_set_hide_transition(dst, transition);
+		obs_source_release(transition);
+	}
+	dst->show_transition_duration = src->show_transition_duration;
+	dst->hide_transition_duration = src->hide_transition_duration;
+
 	if (duplicate_hotkeys && !dst_scene->source->context.private) {
 		obs_data_array_t *data0 = NULL;
 		obs_data_array_t *data1 = NULL;
@@ -1776,6 +1940,10 @@ static void obs_sceneitem_destroy(obs_sceneitem_t *item)
 		signal_handler_disconnect(
 			obs_source_get_signal_handler(item->source), "rename",
 			sceneitem_renamed, item);
+		if (item->show_transition)
+			obs_source_release(item->show_transition);
+		if (item->hide_transition)
+			obs_source_release(item->hide_transition);
 		if (item->source)
 			obs_source_release(item->source);
 		da_free(item->audio_actions);
@@ -1822,6 +1990,9 @@ void obs_sceneitem_remove(obs_sceneitem_t *item)
 
 	set_visibility(item, false);
 
+	obs_sceneitem_set_show_transition(item, NULL);
+	obs_sceneitem_set_hide_transition(item, NULL);
+
 	signal_item_remove(item);
 	detach_sceneitem(item);
 
@@ -2128,6 +2299,18 @@ bool obs_sceneitem_visible(const obs_sceneitem_t *item)
 	return item ? item->user_visible : false;
 }
 
+static bool group_item_transition(obs_scene_t *scene, obs_sceneitem_t *item,
+				  void *param)
+{
+	if (!param || !item)
+		return true;
+	const bool visible = *(bool *)param;
+	if (obs_sceneitem_visible(item))
+		obs_sceneitem_do_transition(item, visible);
+	UNUSED_PARAMETER(scene);
+	return true;
+}
+
 bool obs_sceneitem_set_visible(obs_sceneitem_t *item, bool visible)
 {
 	struct calldata cd;
@@ -2144,6 +2327,13 @@ bool obs_sceneitem_set_visible(obs_sceneitem_t *item, bool visible)
 	if (!item->parent)
 		return false;
 
+	obs_sceneitem_do_transition(item, visible);
+	if (obs_sceneitem_is_group(item))
+		obs_sceneitem_group_enum_items(item, group_item_transition,
+					       &visible);
+
+	item->user_visible = visible;
+
 	if (visible) {
 		if (os_atomic_inc_long(&item->active_refs) == 1) {
 			if (!obs_source_add_active_child(item->parent->source,
@@ -2154,8 +2344,6 @@ bool obs_sceneitem_set_visible(obs_sceneitem_t *item, bool visible)
 		}
 	}
 
-	item->user_visible = visible;
-
 	calldata_init_fixed(&cd, stack, sizeof(stack));
 	calldata_set_ptr(&cd, "item", item);
 	calldata_set_bool(&cd, "visible", visible);
@@ -3013,3 +3201,137 @@ void obs_sceneitem_force_update_transform(obs_sceneitem_t *item)
 	if (os_atomic_set_bool(&item->update_transform, false))
 		update_item_transform(item, false);
 }
+
+void obs_sceneitem_set_show_transition(obs_sceneitem_t *item,
+				       obs_source_t *transition)
+{
+	if (!item)
+		return;
+	if (item->show_transition)
+		obs_source_release(item->show_transition);
+
+	item->show_transition = transition;
+	if (item->show_transition)
+		obs_source_addref(item->show_transition);
+}
+
+void obs_sceneitem_set_show_transition_duration(obs_sceneitem_t *item,
+						uint32_t duration_ms)
+{
+	if (!item)
+		return;
+	item->show_transition_duration = duration_ms;
+}
+
+obs_source_t *obs_sceneitem_get_show_transition(obs_sceneitem_t *item)
+{
+	if (!item)
+		return NULL;
+	return item->show_transition;
+}
+
+uint32_t obs_sceneitem_get_show_transition_duration(obs_sceneitem_t *item)
+{
+	if (!item)
+		return 0;
+	return item->show_transition_duration;
+}
+
+void obs_sceneitem_set_hide_transition(obs_sceneitem_t *item,
+				       obs_source_t *transition)
+{
+	if (!item)
+		return;
+	if (item->hide_transition)
+		obs_source_release(item->hide_transition);
+
+	item->hide_transition = transition;
+	if (item->hide_transition)
+		obs_source_addref(item->hide_transition);
+}
+
+void obs_sceneitem_set_hide_transition_duration(obs_sceneitem_t *item,
+						uint32_t duration_ms)
+{
+	if (!item)
+		return;
+	item->hide_transition_duration = duration_ms;
+}
+
+obs_source_t *obs_sceneitem_get_hide_transition(obs_sceneitem_t *item)
+{
+	if (!item)
+		return NULL;
+	return item->hide_transition;
+}
+
+uint32_t obs_sceneitem_get_hide_transition_duration(obs_sceneitem_t *item)
+{
+	if (!item)
+		return 0;
+	return item->hide_transition_duration;
+}
+
+void obs_sceneitem_transition_stop(void *data, calldata_t *calldata)
+{
+	obs_source_t *parent = data;
+	obs_source_t *transition;
+	calldata_get_ptr(calldata, "source", &transition);
+	obs_source_remove_active_child(parent, transition);
+	signal_handler_t *sh = obs_source_get_signal_handler(transition);
+	if (sh)
+		signal_handler_disconnect(sh, "transition_stop",
+					  obs_sceneitem_transition_stop,
+					  parent);
+}
+
+void obs_sceneitem_do_transition(obs_sceneitem_t *item, bool visible)
+{
+	if (!item)
+		return;
+
+	if (transition_active(item->show_transition))
+		obs_transition_force_stop(item->show_transition);
+
+	if (transition_active(item->hide_transition))
+		obs_transition_force_stop(item->hide_transition);
+
+	obs_source_t *transition =
+		visible ? obs_sceneitem_get_show_transition(item)
+			: obs_sceneitem_get_hide_transition(item);
+	if (!transition)
+		return;
+
+	int duration =
+		(int)(visible ? obs_sceneitem_get_show_transition_duration(item)
+			      : obs_sceneitem_get_hide_transition_duration(
+					item));
+
+	const int cx = obs_source_get_width(item->source);
+	const int cy = obs_source_get_height(item->source);
+	obs_transition_set_size(transition, cx, cy);
+	obs_transition_set_alignment(transition, OBS_ALIGN_CENTER);
+	obs_transition_set_scale_type(transition, OBS_TRANSITION_SCALE_ASPECT);
+
+	if (duration == 0)
+		duration = 300;
+
+	obs_scene_t *scene = obs_sceneitem_get_scene(item);
+	obs_source_t *parent = obs_scene_get_source(scene);
+	obs_source_add_active_child(parent, transition);
+
+	signal_handler_t *sh = obs_source_get_signal_handler(transition);
+	if (sh)
+		signal_handler_connect(sh, "transition_stop",
+				       obs_sceneitem_transition_stop, parent);
+
+	if (!visible) {
+		obs_transition_set(transition, item->source);
+		obs_transition_start(transition, OBS_TRANSITION_MODE_AUTO,
+				     duration, NULL);
+	} else {
+		obs_transition_set(transition, NULL);
+		obs_transition_start(transition, OBS_TRANSITION_MODE_AUTO,
+				     duration, item->source);
+	}
+}

+ 5 - 0
libobs/obs-scene.h

@@ -78,6 +78,11 @@ struct obs_scene_item {
 	pthread_mutex_t actions_mutex;
 	DARRAY(struct item_action) audio_actions;
 
+	struct obs_source *show_transition;
+	struct obs_source *hide_transition;
+	uint32_t show_transition_duration;
+	uint32_t hide_transition_duration;
+
 	/* would do **prev_next, but not really great for reordering */
 	struct obs_scene_item *prev;
 	struct obs_scene_item *next;

+ 16 - 0
libobs/obs.h

@@ -1716,6 +1716,22 @@ EXPORT obs_scene_t *obs_group_from_source(const obs_source_t *source);
 EXPORT void obs_sceneitem_defer_group_resize_begin(obs_sceneitem_t *item);
 EXPORT void obs_sceneitem_defer_group_resize_end(obs_sceneitem_t *item);
 
+EXPORT void obs_sceneitem_set_show_transition(obs_sceneitem_t *item,
+					      obs_source_t *transition);
+EXPORT void obs_sceneitem_set_show_transition_duration(obs_sceneitem_t *item,
+						       uint32_t duration_ms);
+EXPORT obs_source_t *obs_sceneitem_get_show_transition(obs_sceneitem_t *item);
+EXPORT uint32_t
+obs_sceneitem_get_show_transition_duration(obs_sceneitem_t *item);
+EXPORT void obs_sceneitem_set_hide_transition(obs_sceneitem_t *item,
+					      obs_source_t *transition);
+EXPORT void obs_sceneitem_set_hide_transition_duration(obs_sceneitem_t *item,
+						       uint32_t duration_ms);
+EXPORT obs_source_t *obs_sceneitem_get_hide_transition(obs_sceneitem_t *item);
+EXPORT uint32_t
+obs_sceneitem_get_hide_transition_duration(obs_sceneitem_t *item);
+EXPORT void obs_sceneitem_do_transition(obs_sceneitem_t *item, bool visible);
+
 /* ------------------------------------------------------------------------- */
 /* Outputs */