Browse Source

libobs: Introduce Canvases

This adds a new obs_canvas object that acts as a shareable
(reference-counted) owner of views, mixes, and (optionally) scenes.

This is a step towards faciliatating multi-canvas and multi-output
features in OBS Studio.

It solves a number of complications that exist with the manual approach
of using views, such as audio mixing, source active-state tracking, and
scenes not havinga  reliable way of identifying the actual available
canvas size.
Dennis Sädtler 9 months ago
parent
commit
3d57b1fd63
10 changed files with 1043 additions and 132 deletions
  1. 1 0
      libobs/CMakeLists.txt
  2. 1 1
      libobs/obs-audio.c
  3. 555 0
      libobs/obs-canvas.c
  4. 67 6
      libobs/obs-internal.h
  5. 55 15
      libobs/obs-scene.c
  6. 54 13
      libobs/obs-source.c
  7. 5 0
      libobs/obs-source.h
  8. 2 14
      libobs/obs-view.c
  9. 189 83
      libobs/obs.c
  10. 114 0
      libobs/obs.h

+ 1 - 0
libobs/CMakeLists.txt

@@ -38,6 +38,7 @@ target_sources(
     obs-av1.h
     obs-avc.c
     obs-avc.h
+    obs-canvas.c
     obs-config.h
     obs-data.c
     obs-data.h

+ 1 - 1
libobs/obs-audio.c

@@ -512,7 +512,7 @@ bool audio_callback(void *param, uint64_t start_ts_in, uint64_t end_ts_in, uint6
 			obs_source_enum_active_tree(source, push_audio_tree, audio);
 			push_audio_tree(NULL, source, audio);
 
-			if (obs->video.mixes.array[j] == obs->video.main_mix)
+			if (obs->video.mixes.array[j]->mix_audio)
 				da_push_back(audio->root_nodes, &source);
 		}
 		pthread_mutex_unlock(&view->channels_mutex);

+ 555 - 0
libobs/obs-canvas.c

@@ -0,0 +1,555 @@
+/******************************************************************************
+    Copyright (C) 2025 by Dennis Sädtler <[email protected]>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 2 of the License, or
+    (at your option) any later version.
+
+    This program 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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+******************************************************************************/
+
+#include "obs.h"
+#include "obs-internal.h"
+#include "obs-scene.h"
+
+/* The primary canvas has static name/uuid. */
+static const char *MAIN_CANVAS_NAME = "Main";
+static const char *MAIN_CANVAS_UUID = "6c69626f-6273-4c00-9d88-c5136d61696e";
+/* Internal flag to mark a canvas as removed */
+static const uint32_t REMOVED = 1u << 31;
+
+/*** Signals ***/
+
+static const char *canvas_signals[] = {
+	"void destroy(ptr canvas)",
+	"void remove(ptr canvas)",
+	"void video_reset(ptr canvas)",
+
+	"void source_add(ptr canvas, ptr source)",
+	"void source_remove(ptr canvas, ptr source)",
+
+	"void rename(ptr source, string new_name, string prev_name)",
+
+	"void channel_change(ptr canvas, int channel, in out ptr source, ptr prev_source)",
+
+	NULL,
+};
+
+static inline void canvas_dosignal(obs_canvas_t *canvas, const char *signal_obs, const char *signal_source)
+{
+	struct calldata data;
+	uint8_t stack[128];
+
+	calldata_init_fixed(&data, stack, sizeof(stack));
+	calldata_set_ptr(&data, "canvas", canvas);
+	if (signal_obs)
+		signal_handler_signal(obs->signals, signal_obs, &data);
+	if (signal_source)
+		signal_handler_signal(canvas->context.signals, signal_source, &data);
+}
+
+static inline void canvas_dosignal_source(const char *signal, obs_canvas_t *canvas, obs_source_t *source)
+{
+	struct calldata data;
+	uint8_t stack[128];
+
+	calldata_init_fixed(&data, stack, sizeof(stack));
+	calldata_set_ptr(&data, "canvas", canvas);
+	calldata_set_ptr(&data, "source", source);
+
+	signal_handler_signal(source->context.signals, signal, &data);
+}
+
+/*** Reference Counting ***/
+
+void obs_canvas_release(obs_canvas_t *canvas)
+{
+	if (!obs && canvas) {
+		blog(LOG_WARNING, "Tried to release a canvas when the OBS core is shut down!");
+		return;
+	}
+
+	if (!canvas)
+		return;
+
+	obs_weak_canvas_t *control = (obs_weak_canvas_t *)canvas->context.control;
+	if (obs_ref_release(&control->ref)) {
+		obs_canvas_destroy(canvas);
+		obs_weak_canvas_release(control);
+	}
+}
+
+void obs_weak_canvas_addref(obs_weak_canvas_t *weak)
+{
+	if (!weak)
+		return;
+
+	obs_weak_ref_addref(&weak->ref);
+}
+
+void obs_weak_canvas_release(obs_weak_canvas_t *weak)
+{
+	if (!weak)
+		return;
+
+	if (obs_weak_ref_release(&weak->ref))
+		bfree(weak);
+}
+
+obs_canvas_t *obs_canvas_get_ref(obs_canvas_t *canvas)
+{
+	if (!canvas)
+		return NULL;
+
+	return obs_weak_canvas_get_canvas((obs_weak_canvas_t *)canvas->context.control);
+}
+
+obs_weak_canvas_t *obs_canvas_get_weak_canvas(obs_canvas_t *canvas)
+{
+	if (!canvas)
+		return NULL;
+
+	obs_weak_canvas_t *weak = (obs_weak_canvas_t *)canvas->context.control;
+	obs_weak_canvas_addref(weak);
+	return weak;
+}
+
+obs_canvas_t *obs_weak_canvas_get_canvas(obs_weak_canvas_t *weak)
+{
+	if (!weak)
+		return NULL;
+
+	if (obs_weak_ref_get_ref(&weak->ref))
+		return weak->canvas;
+
+	return NULL;
+}
+
+/*** Creation / Destruction ***/
+
+static obs_canvas_t *obs_canvas_create_internal(const char *name, const char *uuid, struct obs_video_info *ovi,
+						uint32_t flags, bool private)
+{
+	struct obs_canvas *canvas = bzalloc(sizeof(struct obs_canvas));
+	canvas->flags = flags;
+
+	if (!obs_context_data_init(&canvas->context, OBS_OBJ_TYPE_CANVAS, NULL, name, uuid, NULL, private))
+		return NULL;
+
+	if (!signal_handler_add_array(canvas->context.signals, canvas_signals)) {
+		obs_context_data_free(&canvas->context);
+		bfree(canvas);
+		return NULL;
+	}
+
+	if (pthread_mutex_init_recursive(&canvas->sources_mutex) != 0) {
+		obs_context_data_free(&canvas->context);
+		bfree(canvas);
+		return NULL;
+	}
+
+	obs_view_init(&canvas->view, flags & ACTIVATE ? MAIN_VIEW : AUX_VIEW);
+	obs_context_init_control(&canvas->context, canvas, (obs_destroy_cb)obs_canvas_destroy);
+
+	/* A canvas can be created without a mix. */
+	if (ovi) {
+		canvas->ovi = *ovi;
+		canvas->mix = obs_create_video_mix(ovi);
+		if (canvas->mix) {
+			canvas->mix->view = &canvas->view;
+			canvas->mix->mix_audio = (flags & MIX_AUDIO) != 0;
+
+			pthread_mutex_lock(&obs->video.mixes_mutex);
+			da_push_back(obs->video.mixes, &canvas->mix);
+			pthread_mutex_unlock(&obs->video.mixes_mutex);
+		}
+	}
+
+	obs_context_data_insert_uuid(&canvas->context, &obs->data.canvases_mutex, &obs->data.canvases);
+	if (!private) {
+		obs_context_data_insert_name(&canvas->context, &obs->data.canvases_mutex, &obs->data.named_canvases);
+		canvas_dosignal(canvas, "canvas_create", NULL);
+	}
+
+	blog(LOG_DEBUG, "%scanvas '%s' (%s) created", private ? "private " : "", canvas->context.name,
+	     canvas->context.uuid);
+
+	return canvas;
+}
+
+obs_canvas_t *obs_create_main_canvas(void)
+{
+	const uint32_t main_flags = MAIN | PROGRAM;
+	return obs_canvas_create_internal(MAIN_CANVAS_NAME, MAIN_CANVAS_UUID, NULL, main_flags, false);
+}
+
+obs_canvas_t *obs_canvas_create(const char *name, struct obs_video_info *ovi, uint32_t flags)
+{
+	flags &= ~MAIN; /* Prevent user from creating a MAIN canvas. */
+	return obs_canvas_create_internal(name, NULL, ovi, flags, false);
+}
+
+obs_canvas_t *obs_canvas_create_private(const char *name, struct obs_video_info *ovi, uint32_t flags)
+{
+	flags &= ~MAIN; /* Prevent user from creating a MAIN canvas. */
+	return obs_canvas_create_internal(name, NULL, ovi, flags, true);
+}
+
+void obs_canvas_destroy(obs_canvas_t *canvas)
+{
+	canvas_dosignal(canvas, "canvas_destroy", "destroy");
+
+	obs_canvas_clear_mix(canvas);
+
+	obs_source_t *source = canvas->sources;
+	while (source) {
+		/* Canvases can hold strong refs to scene sources, release them here. */
+		if (canvas->flags & SCENE_REF && obs_source_is_scene(source))
+			obs_source_release(source);
+
+		source = source->context.hh.next;
+	}
+
+	obs_context_data_remove_uuid(&canvas->context, &obs->data.canvases_mutex, &obs->data.canvases);
+	if (!canvas->context.private) {
+		obs_context_data_remove_name(&canvas->context, &obs->data.canvases_mutex, &obs->data.named_canvases);
+	}
+
+	blog(LOG_DEBUG, "%scanvas '%s' (%s) destroyed", canvas->context.private ? "private " : "", canvas->context.name,
+	     canvas->context.uuid);
+
+	pthread_mutex_destroy(&canvas->sources_mutex);
+	obs_context_data_free(&canvas->context);
+	obs_view_free(&canvas->view);
+	bfree(canvas);
+}
+
+/*** Saving / Loading ***/
+
+obs_data_t *obs_save_canvas(obs_canvas_t *canvas)
+{
+	if (canvas->flags & (EPHEMERAL | REMOVED))
+		return NULL;
+
+	obs_data_t *canvas_data = obs_data_create();
+
+	obs_data_set_string(canvas_data, "name", canvas->context.name);
+	obs_data_set_string(canvas_data, "uuid", canvas->context.uuid);
+	obs_data_set_bool(canvas_data, "private", canvas->context.private);
+	obs_data_set_int(canvas_data, "flags", canvas->flags);
+
+	return canvas_data;
+}
+
+obs_canvas_t *obs_load_canvas(obs_data_t *data)
+{
+	const char *name = obs_data_get_string(data, "name");
+	const char *uuid = obs_data_get_string(data, "uuid");
+	const bool private = obs_data_get_bool(data, "private");
+	uint32_t flags = (uint32_t)obs_data_get_int(data, "flags");
+
+	flags &= ~MAIN; /* Prevent user from creating a MAIN canvas. */
+	return obs_canvas_create_internal(name, uuid, NULL, flags, private);
+}
+
+/*** Internal API ***/
+
+/* Free canvas mix (if any) */
+void obs_canvas_clear_mix(obs_canvas_t *canvas)
+{
+	if (!canvas->mix)
+		return;
+
+	pthread_mutex_lock(&obs->video.mixes_mutex);
+	for (size_t i = 0; i < obs->video.mixes.num; i++) {
+		struct obs_core_video_mix *mix = obs->video.mixes.array[i];
+		if (mix == canvas->mix) {
+			da_erase(obs->video.mixes, i);
+			obs_free_video_mix(mix);
+			break;
+		}
+	}
+	pthread_mutex_unlock(&obs->video.mixes_mutex);
+
+	canvas->mix = NULL;
+}
+
+/* Clear mixes attached to canvases */
+void obs_free_canvas_mixes(void)
+{
+	pthread_mutex_lock(&obs->data.canvases_mutex);
+	struct obs_context_data *ctx, *tmp;
+	HASH_ITER (hh, (struct obs_context_data *)obs->data.canvases, ctx, tmp) {
+		obs_canvas_t *canvas = (obs_canvas_t *)ctx;
+		obs_canvas_clear_mix(canvas);
+	}
+	pthread_mutex_unlock(&obs->data.canvases_mutex);
+}
+
+bool obs_canvas_reset_video_internal(obs_canvas_t *canvas, struct obs_video_info *ovi)
+{
+	obs_canvas_clear_mix(canvas);
+
+	if (ovi)
+		canvas->ovi = *ovi;
+
+	canvas->mix = obs_create_video_mix(&canvas->ovi);
+	if (canvas->mix) {
+		canvas->mix->view = &canvas->view;
+		canvas->mix->mix_audio = (canvas->flags & MIX_AUDIO) != 0;
+
+		pthread_mutex_lock(&obs->video.mixes_mutex);
+		da_push_back(obs->video.mixes, &canvas->mix);
+		pthread_mutex_unlock(&obs->video.mixes_mutex);
+	}
+
+	canvas_dosignal(canvas, "canvas_video_reset", "video_reset");
+
+	return !!canvas->mix;
+}
+
+void obs_canvas_insert_source(obs_canvas_t *canvas, obs_source_t *source)
+{
+	if (canvas->flags & SCENE_REF && obs_source_is_scene(source))
+		obs_source_get_ref(source);
+	if (source->canvas)
+		obs_canvas_remove_source(source);
+
+	source->canvas = obs_canvas_get_weak_canvas(canvas);
+	obs_context_data_insert_name(&source->context, &canvas->sources_mutex, &canvas->sources);
+	canvas_dosignal_source("source_add", canvas, source);
+}
+
+static bool remove_groups_items_cb(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
+{
+	UNUSED_PARAMETER(scene);
+
+	obs_source_t *source = param;
+	if (item->source == source)
+		obs_sceneitem_remove(item);
+
+	return true;
+}
+
+static bool remove_groups_enum_cb(void *param, obs_source_t *scene_source)
+{
+	obs_source_t *source = param;
+	obs_scene_t *scene = obs_scene_from_source(scene_source);
+
+	obs_scene_enum_items(scene, remove_groups_items_cb, source);
+	return true;
+}
+
+void obs_canvas_remove_source(obs_source_t *source)
+{
+	obs_canvas_t *canvas = obs_weak_canvas_get_canvas(source->canvas);
+	if (canvas) {
+		obs_weak_canvas_release(source->canvas);
+		obs_context_data_remove_name(&source->context, &canvas->sources_mutex, &canvas->sources);
+
+		canvas_dosignal_source("source_remove", canvas, source);
+		if (canvas->flags & SCENE_REF && obs_source_is_scene(source))
+			obs_source_release(source);
+
+		/* If source is a group, also remove it from all other scenes in the old canvas */
+		if (obs_source_is_group(source))
+			obs_canvas_enum_scenes(canvas, remove_groups_enum_cb, source);
+
+		obs_canvas_release(canvas);
+	}
+	source->canvas = NULL;
+}
+
+/*** Public Canvas Object API ***/
+
+bool obs_canvas_reset_video(obs_canvas_t *canvas, struct obs_video_info *ovi)
+{
+	if (canvas->flags & MAIN || obs_video_active())
+		return false;
+
+	return obs_canvas_reset_video_internal(canvas, ovi);
+}
+
+video_t *obs_canvas_get_video(const obs_canvas_t *canvas)
+{
+	return canvas->mix ? canvas->mix->video : NULL;
+}
+
+bool obs_canvas_get_video_info(const obs_canvas_t *canvas, struct obs_video_info *ovi)
+{
+	if (!obs->video.graphics || !canvas->mix)
+		return false;
+
+	*ovi = canvas->ovi;
+	return true;
+}
+
+void obs_canvas_set_channel(obs_canvas_t *canvas, uint32_t channel, obs_source_t *source)
+{
+	assert(channel < MAX_CHANNELS);
+
+	if (channel >= MAX_CHANNELS)
+		return;
+
+	struct obs_view *view = &canvas->view;
+
+	pthread_mutex_lock(&view->channels_mutex);
+
+	source = obs_source_get_ref(source);
+
+	obs_source_t *prev_source = view->channels[channel];
+
+	if (source == prev_source) {
+		obs_source_release(source);
+		pthread_mutex_unlock(&view->channels_mutex);
+		return;
+	}
+
+	struct calldata params = {0};
+	calldata_set_ptr(&params, "canvas", canvas);
+	calldata_set_int(&params, "channel", channel);
+	calldata_set_ptr(&params, "prev_source", prev_source);
+	calldata_set_ptr(&params, "source", source);
+
+	signal_handler_signal(canvas->context.signals, "channel_change", &params);
+	if (canvas->flags & MAIN)
+		signal_handler_signal(obs->signals, "channel_change", &params);
+
+	/* For some reason the original implementation allows overriding the source from the callback,
+	 * so just in case support that here as well. This isn't used anywhere in OBS itself. */
+	calldata_get_ptr(&params, "source", &source);
+	view->channels[channel] = source;
+
+	calldata_free(&params);
+	pthread_mutex_unlock(&view->channels_mutex);
+
+	if (source)
+		obs_source_activate(source, view->type);
+
+	if (prev_source) {
+		obs_source_deactivate(prev_source, view->type);
+		obs_source_release(prev_source);
+	}
+}
+
+obs_source_t *obs_canvas_get_channel(obs_canvas_t *canvas, uint32_t channel)
+{
+	return obs_view_get_source(&canvas->view, channel);
+}
+
+obs_scene_t *obs_canvas_scene_create(obs_canvas_t *canvas, const char *name)
+{
+	struct obs_source *source = obs_source_create_canvas(canvas, "scene", name, NULL, NULL);
+	return source->context.data;
+}
+
+void obs_canvas_scene_remove(obs_scene_t *scene)
+{
+	obs_canvas_remove_source(scene->source);
+}
+
+void obs_canvas_set_name(obs_canvas_t *canvas, const char *name)
+{
+	if (!name || !*name)
+		return;
+	if (canvas->flags & MAIN) /* Do not allow renaming main canvases. */
+		return;
+	if (strcmp(name, canvas->context.name) == 0)
+		return;
+
+	char *prev_name = bstrdup(canvas->context.name);
+
+	if (canvas->context.private)
+		obs_context_data_setname(&canvas->context, name);
+	else
+		obs_context_data_setname_ht(&canvas->context, name, &obs->data.named_canvases);
+
+	struct calldata data;
+	calldata_init(&data);
+	calldata_set_ptr(&data, "canvas", canvas);
+	calldata_set_string(&data, "new_name", canvas->context.name);
+	calldata_set_string(&data, "prev_name", prev_name);
+	signal_handler_signal(canvas->context.signals, "rename", &data);
+
+	if (!canvas->context.private)
+		signal_handler_signal(obs->signals, "canvas_rename", &data);
+
+	calldata_free(&data);
+	bfree(prev_name);
+}
+
+const char *obs_canvas_get_name(const obs_canvas_t *canvas)
+{
+	return canvas->context.name;
+}
+
+const char *obs_canvas_get_uuid(const obs_canvas_t *canvas)
+{
+	return canvas->context.uuid;
+}
+
+uint32_t obs_canvas_get_flags(const obs_canvas_t *canvas)
+{
+	return canvas->flags;
+}
+
+static bool enum_move_cb(obs_scene_t *scene, obs_sceneitem_t *item, void *param)
+{
+	UNUSED_PARAMETER(scene);
+
+	obs_canvas_t *dst = param;
+	obs_source_t *source = item->source;
+
+	if (obs_source_is_group(source)) {
+		obs_canvas_remove_source(source);
+		obs_canvas_insert_source(dst, source);
+	}
+
+	return true;
+}
+
+void obs_canvas_move_scene(obs_scene_t *scene, obs_canvas_t *dst)
+{
+	obs_source_t *source = scene->source;
+	obs_canvas_remove_source(source);
+	obs_canvas_insert_source(dst, source);
+
+	/* Also move all groups within this scene */
+	obs_scene_enum_items(scene, enum_move_cb, dst);
+}
+
+void obs_canvas_remove(obs_canvas_t *canvas)
+{
+	/* Do not allow removing the main canvas, or canvases already marked as removed. */
+	if (canvas->flags & (REMOVED | MAIN))
+		return;
+
+	obs_canvas_t *c = obs_canvas_get_ref(canvas);
+	if (c) {
+		c->flags |= REMOVED;
+		canvas_dosignal(c, "canvas_remove", "remove");
+		obs_canvas_release(c);
+	}
+}
+
+bool obs_canvas_removed(obs_canvas_t *canvas)
+{
+	return (canvas->flags & REMOVED) != 0;
+}
+
+bool obs_canvas_has_video(obs_canvas_t *canvas)
+{
+	return canvas->mix != NULL;
+}
+
+void obs_canvas_render(obs_canvas_t *canvas)
+{
+	obs_view_render(&canvas->view);
+}

+ 67 - 6
libobs/obs-internal.h

@@ -325,6 +325,8 @@ struct obs_core_video_mix {
 
 	bool encoder_only_mix;
 	long encoder_refs;
+
+	bool mix_audio;
 };
 
 extern struct obs_core_video_mix *obs_create_video_mix(struct obs_video_info *ovi);
@@ -377,7 +379,6 @@ struct obs_core_video {
 
 	pthread_mutex_t mixes_mutex;
 	DARRAY(struct obs_core_video_mix *) mixes;
-	struct obs_core_video_mix *main_mix;
 };
 
 extern void add_ready_encoder_group(obs_encoder_t *encoder);
@@ -412,6 +413,9 @@ struct obs_core_data {
 	struct obs_source *sources;        /* Lookup by UUID (hh_uuid) */
 	struct obs_source *public_sources; /* Lookup by name (hh) */
 
+	struct obs_canvas *canvases;       /* Lookup by UUID (hh_uuid) */
+	struct obs_canvas *named_canvases; /* Lookup by name (hh) */
+
 	/* Linked lists */
 	struct obs_source *first_audio_source;
 	struct obs_display *first_display;
@@ -426,11 +430,13 @@ struct obs_core_data {
 	pthread_mutex_t services_mutex;
 	pthread_mutex_t audio_sources_mutex;
 	pthread_mutex_t draw_callbacks_mutex;
+	pthread_mutex_t canvases_mutex;
 	DARRAY(struct draw_callback) draw_callbacks;
 	DARRAY(struct rendered_callback) rendered_callbacks;
 	DARRAY(struct tick_callback) tick_callbacks;
 
-	struct obs_view main_view;
+	/* Main canvas, guaranteed to exist for the lifetime of the program */
+	struct obs_canvas *main_canvas;
 
 	long long unnamed_index;
 
@@ -595,8 +601,8 @@ extern void obs_context_data_insert_name(struct obs_context_data *context, pthre
 extern void obs_context_data_insert_uuid(struct obs_context_data *context, pthread_mutex_t *mutex, void *first_uuid);
 
 extern void obs_context_data_remove(struct obs_context_data *context);
-extern void obs_context_data_remove_name(struct obs_context_data *context, void *phead);
-extern void obs_context_data_remove_uuid(struct obs_context_data *context, void *puuid_head);
+extern void obs_context_data_remove_name(struct obs_context_data *context, pthread_mutex_t *mutex, void *phead);
+extern void obs_context_data_remove_uuid(struct obs_context_data *context, pthread_mutex_t *mutex, void *puuid_head);
 
 extern void obs_context_wait(struct obs_context_data *context);
 
@@ -644,6 +650,40 @@ static inline bool obs_weak_ref_expired(struct obs_weak_ref *ref)
 	return owners < 0;
 }
 
+/* ------------------------------------------------------------------------- */
+/* canvases */
+
+struct obs_weak_canvas {
+	struct obs_weak_ref ref;
+	struct obs_canvas *canvas;
+};
+
+struct obs_canvas {
+	struct obs_context_data context;
+
+	/* obs_canvas_flags */
+	uint32_t flags;
+	/* Video info for this canvas, FPS ignored */
+	struct obs_video_info ovi;
+
+	/* Hash table containing scenes (and groups) associated with this canvas */
+	struct obs_source *sources;
+	pthread_mutex_t sources_mutex;
+
+	/* For now, canvas objects mainly act as a proxy for the existing view and video mix objects,
+	 * though this may change in the future. */
+	struct obs_view view;
+	struct obs_core_video_mix *mix;
+};
+
+extern obs_canvas_t *obs_create_main_canvas(void);
+extern void obs_canvas_destroy(obs_canvas_t *canvas);
+extern void obs_canvas_clear_mix(obs_canvas_t *canvas);
+extern void obs_free_canvas_mixes(void);
+extern bool obs_canvas_reset_video_internal(obs_canvas_t *canvas, struct obs_video_info *ovi);
+extern void obs_canvas_insert_source(obs_canvas_t *canvas, obs_source_t *source);
+extern void obs_canvas_remove_source(obs_source_t *source);
+
 /* ------------------------------------------------------------------------- */
 /* sources  */
 
@@ -894,6 +934,9 @@ struct obs_source {
 
 	/* private data */
 	obs_data_t *private_settings;
+
+	/* canvas this source belongs to (only used for scenes) */
+	obs_weak_canvas_t *canvas;
 };
 
 extern struct obs_source_info *get_source_info(const char *id);
@@ -912,9 +955,12 @@ struct audio_monitor *audio_monitor_create(obs_source_t *source);
 void audio_monitor_reset(struct audio_monitor *monitor);
 extern void audio_monitor_destroy(struct audio_monitor *monitor);
 
-extern obs_source_t *obs_source_create_set_last_ver(const char *id, const char *name, const char *uuid,
-						    obs_data_t *settings, obs_data_t *hotkey_data,
+extern obs_source_t *obs_source_create_canvas(obs_canvas_t *canvas, const char *id, const char *name,
+					      obs_data_t *settings, obs_data_t *hotkey_data);
+extern obs_source_t *obs_source_create_set_last_ver(obs_canvas_t *canvas, const char *id, const char *name,
+						    const char *uuid, obs_data_t *settings, obs_data_t *hotkey_data,
 						    uint32_t last_obs_ver, bool is_private);
+
 extern void obs_source_destroy(struct obs_source *source);
 extern void obs_source_addref(obs_source_t *source);
 
@@ -931,6 +977,21 @@ static inline void obs_source_dosignal(struct obs_source *source, const char *si
 		signal_handler_signal(source->context.signals, signal_source, &data);
 }
 
+static inline void obs_source_dosignal_canvas(struct obs_source *source, struct obs_canvas *canvas,
+					      const char *signal_obs, const char *signal_source)
+{
+	struct calldata data;
+	uint8_t stack[128];
+
+	calldata_init_fixed(&data, stack, sizeof(stack));
+	calldata_set_ptr(&data, "source", source);
+	calldata_set_ptr(&data, "canvas", canvas);
+	if (signal_obs && !source->context.private)
+		signal_handler_signal(obs->signals, signal_obs, &data);
+	if (signal_source)
+		signal_handler_signal(source->context.signals, signal_source, &data);
+}
+
 /* maximum timestamp variance in nanoseconds */
 #define MAX_TS_VAR 2000000000ULL
 

+ 55 - 15
libobs/obs-scene.c

@@ -338,13 +338,18 @@ void add_alignment(struct vec2 *v, uint32_t align, int cx, int cy)
 
 static uint32_t scene_getwidth(void *data);
 static uint32_t scene_getheight(void *data);
+static uint32_t canvas_getwidth(obs_weak_canvas_t *weak);
+static uint32_t canvas_getheight(obs_weak_canvas_t *weak);
 
 static inline void get_scene_dimensions(const obs_sceneitem_t *item, float *x, float *y)
 {
 	obs_scene_t *parent = item->parent;
-	if (!parent || parent->is_group) {
-		*x = (float)obs->video.main_mix->ovi.base_width;
-		*y = (float)obs->video.main_mix->ovi.base_height;
+	if (!parent) {
+		*x = (float)obs->data.main_canvas->mix->ovi.base_width;
+		*y = (float)obs->data.main_canvas->mix->ovi.base_height;
+	} else if (parent->is_group) {
+		*x = (float)canvas_getwidth(parent->source->canvas);
+		*y = (float)canvas_getheight(parent->source->canvas);
 	} else {
 		*x = (float)scene_getwidth(parent);
 		*y = (float)scene_getheight(parent);
@@ -1455,13 +1460,41 @@ static void scene_save(void *data, obs_data_t *settings)
 	obs_data_array_release(array);
 }
 
+static uint32_t canvas_getwidth(obs_weak_canvas_t *weak)
+{
+	uint32_t width = 0;
+
+	obs_canvas_t *canvas = obs_weak_canvas_get_canvas(weak);
+	if (canvas) {
+		width = canvas->ovi.base_width;
+		obs_canvas_release(canvas);
+	}
+
+	return width;
+}
+
+static uint32_t canvas_getheight(obs_weak_canvas_t *weak)
+{
+	uint32_t height = 0;
+
+	obs_canvas_t *canvas = obs_weak_canvas_get_canvas(weak);
+	if (canvas) {
+		height = canvas->ovi.base_height;
+		obs_canvas_release(canvas);
+	}
+
+	return height;
+}
+
 static uint32_t scene_getwidth(void *data)
 {
 	obs_scene_t *scene = data;
 	if (scene->custom_size)
 		return scene->cx;
-	if (obs->video.main_mix)
-		return obs->video.main_mix->ovi.base_width;
+	if (scene->source->canvas)
+		return canvas_getwidth(scene->source->canvas);
+	if (obs->data.main_canvas->mix)
+		return obs->data.main_canvas->mix->ovi.base_width;
 	return 0;
 }
 
@@ -1470,8 +1503,10 @@ static uint32_t scene_getheight(void *data)
 	obs_scene_t *scene = data;
 	if (scene->custom_size)
 		return scene->cy;
-	if (obs->video.main_mix)
-		return obs->video.main_mix->ovi.base_height;
+	if (scene->source->canvas)
+		return canvas_getheight(scene->source->canvas);
+	if (obs->data.main_canvas->mix)
+		return obs->data.main_canvas->mix->ovi.base_height;
 	return 0;
 }
 
@@ -1793,7 +1828,7 @@ const struct obs_source_info scene_info = {
 	.id = "scene",
 	.type = OBS_SOURCE_TYPE_SCENE,
 	.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW | OBS_SOURCE_COMPOSITE | OBS_SOURCE_DO_NOT_DUPLICATE |
-			OBS_SOURCE_SRGB,
+			OBS_SOURCE_SRGB | OBS_SOURCE_REQUIRES_CANVAS,
 	.get_name = scene_getname,
 	.create = scene_create,
 	.destroy = scene_destroy,
@@ -1812,7 +1847,8 @@ const struct obs_source_info scene_info = {
 const struct obs_source_info group_info = {
 	.id = "group",
 	.type = OBS_SOURCE_TYPE_SCENE,
-	.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW | OBS_SOURCE_COMPOSITE | OBS_SOURCE_SRGB,
+	.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW | OBS_SOURCE_COMPOSITE | OBS_SOURCE_SRGB |
+			OBS_SOURCE_REQUIRES_CANVAS,
 	.get_name = group_getname,
 	.create = scene_create,
 	.destroy = scene_destroy,
@@ -1828,9 +1864,9 @@ const struct obs_source_info group_info = {
 	.video_get_color_space = scene_video_get_color_space,
 };
 
-static inline obs_scene_t *create_id(const char *id, const char *name)
+static inline obs_scene_t *create_id(obs_canvas_t *canvas, const char *id, const char *name)
 {
-	struct obs_source *source = obs_source_create(id, name, NULL, NULL);
+	struct obs_source *source = obs_source_create_canvas(canvas, id, name, NULL, NULL);
 	return source->context.data;
 }
 
@@ -1842,7 +1878,7 @@ static inline obs_scene_t *create_private_id(const char *id, const char *name)
 
 obs_scene_t *obs_scene_create(const char *name)
 {
-	return create_id("scene", name);
+	return create_id(obs->data.main_canvas, "scene", name);
 }
 
 obs_scene_t *obs_scene_create_private(const char *name)
@@ -1987,8 +2023,10 @@ obs_scene_t *obs_scene_duplicate(obs_scene_t *scene, const char *name, enum obs_
 
 	/* --------------------------------- */
 
+	obs_canvas_t *canvas = obs_weak_canvas_get_canvas(scene->source->canvas);
 	new_scene = make_private ? create_private_id(scene->source->info.id, name)
-				 : create_id(scene->source->info.id, name);
+				 : create_id(canvas, scene->source->info.id, name);
+	obs_canvas_release(canvas);
 
 	new_scene->is_group = scene->is_group;
 	new_scene->custom_size = scene->custom_size;
@@ -3630,9 +3668,11 @@ obs_sceneitem_t *obs_scene_insert_group(obs_scene_t *scene, const char *name, ob
 			return NULL;
 	}
 
-	obs_scene_t *sub_scene = create_id("group", name);
-	obs_sceneitem_t *last_item = items ? items[count - 1] : NULL;
+	obs_canvas_t *canvas = obs_weak_canvas_get_canvas(scene->source->canvas);
+	obs_scene_t *sub_scene = create_id(canvas, group_info.id, name);
+	obs_canvas_release(canvas);
 
+	obs_sceneitem_t *last_item = items ? items[count - 1] : NULL;
 	obs_sceneitem_t *item = obs_scene_add_internal(scene, sub_scene->source, last_item, 0);
 
 	if (!items || !count) {

+ 54 - 13
libobs/obs-source.c

@@ -164,6 +164,11 @@ static inline bool is_composite_source(const struct obs_source *source)
 	return source->info.output_flags & OBS_SOURCE_COMPOSITE;
 }
 
+static inline bool requires_canvas(const struct obs_source *source)
+{
+	return source->info.output_flags & OBS_SOURCE_REQUIRES_CANVAS;
+}
+
 extern char *find_libobs_data_file(const char *file);
 
 /* internal initialization */
@@ -218,7 +223,7 @@ static bool obs_source_init(struct obs_source *source)
 	return true;
 }
 
-static void obs_source_init_finalize(struct obs_source *source)
+static void obs_source_init_finalize(struct obs_source *source, obs_canvas_t *canvas)
 {
 	if (is_audio_source(source)) {
 		pthread_mutex_lock(&obs->data.audio_sources_mutex);
@@ -233,7 +238,12 @@ static void obs_source_init_finalize(struct obs_source *source)
 	}
 
 	if (!source->context.private) {
-		obs_context_data_insert_name(&source->context, &obs->data.sources_mutex, &obs->data.public_sources);
+		if (requires_canvas(source)) {
+			obs_canvas_insert_source(canvas, source);
+		} else {
+			obs_context_data_insert_name(&source->context, &obs->data.sources_mutex,
+						     &obs->data.public_sources);
+		}
 	}
 	obs_context_data_insert_uuid(&source->context, &obs->data.sources_mutex, &obs->data.sources);
 }
@@ -320,7 +330,7 @@ static void obs_source_init_audio_hotkeys(struct obs_source *source)
 
 static obs_source_t *obs_source_create_internal(const char *id, const char *name, const char *uuid,
 						obs_data_t *settings, obs_data_t *hotkey_data, bool private,
-						uint32_t last_obs_ver)
+						uint32_t last_obs_ver, obs_canvas_t *canvas)
 {
 	struct obs_source *source = bzalloc(sizeof(struct obs_source));
 
@@ -362,6 +372,12 @@ static obs_source_t *obs_source_create_internal(const char *id, const char *name
 	if (!obs_source_init(source))
 		goto fail;
 
+	/* Scenes need canvases, fall back to using default canvas if none provided here. */
+	if (requires_canvas(source) && !canvas) {
+		blog(LOG_WARNING, "Attempted to add Scene without specifying a canvas! Using default canvas instead.");
+		canvas = obs->data.main_canvas;
+	}
+
 	if (!private)
 		obs_source_init_audio_hotkeys(source);
 
@@ -377,9 +393,12 @@ static obs_source_t *obs_source_create_internal(const char *id, const char *name
 	source->flags = source->default_flags;
 	source->enabled = true;
 
-	obs_source_init_finalize(source);
+	obs_source_init_finalize(source, canvas);
 	if (!private) {
-		obs_source_dosignal(source, "source_create", NULL);
+		if (canvas)
+			obs_source_dosignal_canvas(source, canvas, "source_create_canvas", NULL);
+		if (!canvas || canvas == obs->data.main_canvas)
+			obs_source_dosignal(source, "source_create", NULL);
 	}
 
 	return source;
@@ -392,18 +411,25 @@ fail:
 
 obs_source_t *obs_source_create(const char *id, const char *name, obs_data_t *settings, obs_data_t *hotkey_data)
 {
-	return obs_source_create_internal(id, name, NULL, settings, hotkey_data, false, LIBOBS_API_VER);
+	return obs_source_create_internal(id, name, NULL, settings, hotkey_data, false, LIBOBS_API_VER, NULL);
 }
 
 obs_source_t *obs_source_create_private(const char *id, const char *name, obs_data_t *settings)
 {
-	return obs_source_create_internal(id, name, NULL, settings, NULL, true, LIBOBS_API_VER);
+	return obs_source_create_internal(id, name, NULL, settings, NULL, true, LIBOBS_API_VER, NULL);
+}
+
+obs_source_t *obs_source_create_canvas(obs_canvas_t *canvas, const char *id, const char *name, obs_data_t *settings,
+				       obs_data_t *hotkey_data)
+{
+	return obs_source_create_internal(id, name, NULL, settings, hotkey_data, false, LIBOBS_API_VER, canvas);
 }
 
-obs_source_t *obs_source_create_set_last_ver(const char *id, const char *name, const char *uuid, obs_data_t *settings,
-					     obs_data_t *hotkey_data, uint32_t last_obs_ver, bool is_private)
+obs_source_t *obs_source_create_set_last_ver(obs_canvas_t *canvas, const char *id, const char *name, const char *uuid,
+					     obs_data_t *settings, obs_data_t *hotkey_data, uint32_t last_obs_ver,
+					     bool is_private)
 {
-	return obs_source_create_internal(id, name, uuid, settings, hotkey_data, is_private, last_obs_ver);
+	return obs_source_create_internal(id, name, uuid, settings, hotkey_data, is_private, last_obs_ver, canvas);
 }
 
 static char *get_new_filter_name(obs_source_t *dst, const char *name)
@@ -612,9 +638,15 @@ void obs_source_destroy(struct obs_source *source)
 	while (source->filters.num)
 		obs_source_filter_remove(source, source->filters.array[0]);
 
-	obs_context_data_remove_uuid(&source->context, &obs->data.sources);
-	if (!source->context.private)
-		obs_context_data_remove_name(&source->context, &obs->data.public_sources);
+	obs_context_data_remove_uuid(&source->context, &obs->data.sources_mutex, &obs->data.sources);
+	if (!source->context.private) {
+		if (requires_canvas(source)) {
+			obs_canvas_remove_source(source);
+		} else {
+			obs_context_data_remove_name(&source->context, &obs->data.sources_mutex,
+						     &obs->data.public_sources);
+		}
+	}
 
 	source_profiler_remove_source(source);
 
@@ -794,6 +826,10 @@ void obs_source_remove(obs_source_t *source)
 		if (s) {
 			s->removed = true;
 			obs_source_dosignal(s, "source_remove", "remove");
+			/* Remove from canvas if there is one. */
+			if (source->canvas)
+				obs_canvas_remove_source(s);
+
 			obs_source_release(s);
 		}
 	}
@@ -5829,3 +5865,8 @@ uint64_t obs_source_get_last_async_ts(const obs_source_t *source)
 {
 	return source->async_last_rendered_ts;
 }
+
+obs_canvas_t *obs_source_get_canvas(const obs_source_t *source)
+{
+	return obs_weak_canvas_get_canvas(source->canvas);
+}

+ 5 - 0
libobs/obs-source.h

@@ -203,6 +203,11 @@ enum obs_media_state {
  */
 #define OBS_SOURCE_CAP_DONT_SHOW_PROPERTIES (1 << 16)
 
+/**
+ * Source requires a canvas to operate
+ */
+#define OBS_SOURCE_REQUIRES_CANVAS (1 << 17)
+
 /** @} */
 
 typedef void (*obs_source_enum_proc_t)(obs_source_t *parent, obs_source_t *child, void *param);

+ 2 - 14
libobs/obs-view.c

@@ -150,21 +150,11 @@ static inline size_t find_mix_for_view(obs_view_t *view)
 	return DARRAY_INVALID;
 }
 
-static inline void set_main_mix()
-{
-	size_t idx = find_mix_for_view(&obs->data.main_view);
-
-	struct obs_core_video_mix *mix = NULL;
-	if (idx != DARRAY_INVALID)
-		mix = obs->video.mixes.array[idx];
-	obs->video.main_mix = mix;
-}
-
 video_t *obs_view_add(obs_view_t *view)
 {
-	if (!obs->video.main_mix)
+	if (!obs->data.main_canvas->mix)
 		return NULL;
-	return obs_view_add2(view, &obs->video.main_mix->ovi);
+	return obs_view_add2(view, &obs->data.main_canvas->mix->ovi);
 }
 
 video_t *obs_view_add2(obs_view_t *view, struct obs_video_info *ovi)
@@ -180,7 +170,6 @@ video_t *obs_view_add2(obs_view_t *view, struct obs_video_info *ovi)
 
 	pthread_mutex_lock(&obs->video.mixes_mutex);
 	da_push_back(obs->video.mixes, &mix);
-	set_main_mix();
 	pthread_mutex_unlock(&obs->video.mixes_mutex);
 
 	return mix->video;
@@ -196,7 +185,6 @@ void obs_view_remove(obs_view_t *view)
 		if (obs->video.mixes.array[i]->view == view)
 			obs->video.mixes.array[i]->view = NULL;
 	}
-	set_main_mix();
 	pthread_mutex_unlock(&obs->video.mixes_mutex);
 }
 

+ 189 - 83
libobs/obs.c

@@ -615,8 +615,8 @@ static int obs_init_video_mix(struct obs_video_info *ovi, struct obs_core_video_
 	 * so share FPS settings for aux views */
 	pthread_mutex_lock(&obs->video.mixes_mutex);
 	size_t num = obs->video.mixes.num;
-	if (num && obs->video.main_mix) {
-		struct obs_video_info main_ovi = obs->video.main_mix->ovi;
+	if (num && obs->data.main_canvas->mix) {
+		struct obs_video_info main_ovi = obs->data.main_canvas->mix->ovi;
 		video->ovi.fps_num = main_ovi.fps_num;
 		video->ovi.fps_den = main_ovi.fps_den;
 	}
@@ -665,6 +665,27 @@ struct obs_core_video_mix *obs_create_video_mix(struct obs_video_info *ovi)
 	return video;
 }
 
+static bool restore_canvases(void)
+{
+	bool success = true;
+
+	pthread_mutex_lock(&obs->data.canvases_mutex);
+	struct obs_context_data *ctx, *tmp;
+	HASH_ITER (hh, (struct obs_context_data *)obs->data.canvases, ctx, tmp) {
+		obs_canvas_t *canvas = (obs_canvas_t *)ctx;
+		if (canvas->flags & MAIN)
+			continue;
+
+		if (!obs_canvas_reset_video_internal(canvas, NULL)) {
+			blog(LOG_ERROR, "Failed restoring video mix for canvas '%s'", canvas->context.name);
+			success = false;
+		}
+	}
+	pthread_mutex_unlock(&obs->data.canvases_mutex);
+
+	return success;
+}
+
 static int obs_init_video(struct obs_video_info *ovi)
 {
 	struct obs_core_video *video = &obs->video;
@@ -678,7 +699,11 @@ static int obs_init_video(struct obs_video_info *ovi)
 	if (pthread_mutex_init(&video->mixes_mutex, NULL) < 0)
 		return OBS_VIDEO_FAIL;
 
-	if (!obs_view_add2(&obs->data.main_view, ovi))
+	/* Reset main canvas mix first so it remains first in the rendering order. */
+	if (!obs_canvas_reset_video_internal(obs->data.main_canvas, ovi))
+		return OBS_VIDEO_FAIL;
+	/* Reset mixes for remaining canvases using their existing video info. */
+	if (!restore_canvases())
 		return OBS_VIDEO_FAIL;
 
 	int errorcode;
@@ -939,12 +964,13 @@ static bool obs_init_data(void)
 		goto fail;
 	if (pthread_mutex_init_recursive(&obs->data.draw_callbacks_mutex) != 0)
 		goto fail;
-
-	if (!obs_view_init(&data->main_view))
+	if (pthread_mutex_init_recursive(&obs->data.canvases_mutex) != 0)
 		goto fail;
 
 	data->sources = NULL;
 	data->public_sources = NULL;
+	data->canvases = NULL;
+	data->named_canvases = NULL;
 	data->private_data = obs_data_create();
 	data->valid = true;
 
@@ -993,11 +1019,11 @@ static void obs_free_data(void)
 
 	data->valid = false;
 
-	obs_view_remove(&data->main_view);
-	obs_main_view_free(&data->main_view);
-
 	blog(LOG_INFO, "Freeing OBS context data");
 
+	/* Free main canvas */
+	obs_canvas_release(data->main_canvas);
+
 	FREE_OBS_LINKED_LIST(output);
 	FREE_OBS_LINKED_LIST(encoder);
 	FREE_OBS_LINKED_LIST(display);
@@ -1005,6 +1031,8 @@ static void obs_free_data(void)
 
 	FREE_OBS_HASH_TABLE(hh, &data->public_sources, source);
 	FREE_OBS_HASH_TABLE(hh_uuid, &data->sources, source);
+	FREE_OBS_HASH_TABLE(hh, &data->named_canvases, canvas);
+	FREE_OBS_HASH_TABLE(hh_uuid, &data->canvases, canvas);
 
 	os_task_queue_wait(obs->destruction_task_thread);
 
@@ -1015,6 +1043,7 @@ static void obs_free_data(void)
 	pthread_mutex_destroy(&data->encoders_mutex);
 	pthread_mutex_destroy(&data->services_mutex);
 	pthread_mutex_destroy(&data->draw_callbacks_mutex);
+	pthread_mutex_destroy(&data->canvases_mutex);
 	da_free(data->draw_callbacks);
 	da_free(data->rendered_callbacks);
 	da_free(data->tick_callbacks);
@@ -1028,6 +1057,7 @@ static void obs_free_data(void)
 
 static const char *obs_signals[] = {
 	"void source_create(ptr source)",
+	"void source_create_canvas(ptr source, ptr canvas)",
 	"void source_destroy(ptr source)",
 	"void source_remove(ptr source)",
 	"void source_update(ptr source)",
@@ -1043,8 +1073,7 @@ static const char *obs_signals[] = {
 	"void source_filter_remove(ptr source, ptr filter)",
 	"void source_rename(ptr source, string new_name, string prev_name)",
 	"void source_volume(ptr source, in out float volume)",
-	"void source_volume_level(ptr source, float level, float magnitude, "
-	"float peak)",
+	"void source_volume_level(ptr source, float level, float magnitude, float peak)",
 	"void source_transition_start(ptr source)",
 	"void source_transition_video_stop(ptr source)",
 	"void source_transition_stop(ptr source)",
@@ -1056,6 +1085,12 @@ static const char *obs_signals[] = {
 	"void hotkey_unregister(ptr hotkey)",
 	"void hotkey_bindings_changed(ptr hotkey)",
 
+	"void canvas_create(ptr canvas)",
+	"void canvas_remove(ptr canvas)",
+	"void canvas_destroy(ptr canvas)",
+	"void canvas_video_reset(ptr canvas)",
+	"void canvas_rename(ptr canvas, string new_name, string prev_name)",
+
 	"void video_reset()",
 
 	NULL,
@@ -1189,6 +1224,11 @@ static bool obs_init(const char *locale, const char *module_config_path, profile
 	if (!obs_init_hotkeys())
 		return false;
 
+	/* Create persistent main canvas. */
+	obs->data.main_canvas = obs_create_main_canvas();
+	if (!obs->data.main_canvas)
+		return false;
+
 	obs->destruction_task_thread = os_task_queue_create();
 	if (!obs->destruction_task_thread)
 		return false;
@@ -1453,6 +1493,7 @@ int obs_reset_video(struct obs_video_info *ovi)
 		return OBS_VIDEO_INVALID_PARAM;
 
 	stop_video();
+	obs_free_canvas_mixes();
 	obs_free_video();
 
 	/* align to multiple-of-two and SSE alignment sizes */
@@ -1571,10 +1612,10 @@ bool obs_reset_audio(const struct obs_audio_info *oai)
 
 bool obs_get_video_info(struct obs_video_info *ovi)
 {
-	if (!obs->video.graphics || !obs->video.main_mix)
+	if (!obs->video.graphics || !obs->data.main_canvas->mix)
 		return false;
 
-	*ovi = obs->video.main_mix->ovi;
+	*ovi = obs->data.main_canvas->mix->ovi;
 	return true;
 }
 
@@ -1740,49 +1781,17 @@ audio_t *obs_get_audio(void)
 
 video_t *obs_get_video(void)
 {
-	return obs->video.main_mix->video;
+	return obs->data.main_canvas->mix->video;
 }
 
 obs_source_t *obs_get_output_source(uint32_t channel)
 {
-	return obs_view_get_source(&obs->data.main_view, channel);
+	return obs_canvas_get_channel(obs->data.main_canvas, channel);
 }
 
 void obs_set_output_source(uint32_t channel, obs_source_t *source)
 {
-	assert(channel < MAX_CHANNELS);
-
-	if (channel >= MAX_CHANNELS)
-		return;
-
-	struct obs_source *prev_source;
-	struct obs_view *view = &obs->data.main_view;
-	struct calldata params = {0};
-
-	pthread_mutex_lock(&view->channels_mutex);
-
-	source = obs_source_get_ref(source);
-
-	prev_source = view->channels[channel];
-
-	calldata_set_int(&params, "channel", channel);
-	calldata_set_ptr(&params, "prev_source", prev_source);
-	calldata_set_ptr(&params, "source", source);
-	signal_handler_signal(obs->signals, "channel_change", &params);
-	calldata_get_ptr(&params, "source", &source);
-	calldata_free(&params);
-
-	view->channels[channel] = source;
-
-	pthread_mutex_unlock(&view->channels_mutex);
-
-	if (source)
-		obs_source_activate(source, MAIN_VIEW);
-
-	if (prev_source) {
-		obs_source_deactivate(prev_source, MAIN_VIEW);
-		obs_source_release(prev_source);
-	}
+	obs_canvas_set_channel(obs->data.main_canvas, channel, source);
 }
 
 void obs_enum_sources(bool (*enum_proc)(void *, obs_source_t *), void *param)
@@ -1790,38 +1799,40 @@ void obs_enum_sources(bool (*enum_proc)(void *, obs_source_t *), void *param)
 	obs_source_t *source;
 
 	pthread_mutex_lock(&obs->data.sources_mutex);
-	source = obs->data.public_sources;
+	source = obs->data.sources;
 
 	while (source) {
 		obs_source_t *s = obs_source_get_ref(source);
 		if (s) {
-			if (s->info.type == OBS_SOURCE_TYPE_INPUT && !enum_proc(param, s)) {
-				obs_source_release(s);
-				break;
-			} else if (strcmp(s->info.id, group_info.id) == 0 && !enum_proc(param, s)) {
-				obs_source_release(s);
-				break;
+			if (!s->context.private) {
+				if (s->info.type == OBS_SOURCE_TYPE_INPUT && !enum_proc(param, s)) {
+					obs_source_release(s);
+					break;
+				} else if (strcmp(s->info.id, group_info.id) == 0 && !enum_proc(param, s)) {
+					obs_source_release(s);
+					break;
+				}
 			}
 			obs_source_release(s);
 		}
 
-		source = (obs_source_t *)source->context.hh.next;
+		source = (obs_source_t *)source->context.hh_uuid.next;
 	}
 
 	pthread_mutex_unlock(&obs->data.sources_mutex);
 }
 
-void obs_enum_scenes(bool (*enum_proc)(void *, obs_source_t *), void *param)
+void obs_canvas_enum_scenes(obs_canvas_t *canvas, bool (*enum_proc)(void *, obs_source_t *), void *param)
 {
 	obs_source_t *source;
 
-	pthread_mutex_lock(&obs->data.sources_mutex);
+	pthread_mutex_lock(&canvas->sources_mutex);
 
-	source = obs->data.public_sources;
+	source = canvas->sources;
 	while (source) {
 		obs_source_t *s = obs_source_get_ref(source);
 		if (s) {
-			if (source->info.type == OBS_SOURCE_TYPE_SCENE && !enum_proc(param, s)) {
+			if (obs_source_is_scene(source) && !enum_proc(param, s)) {
 				obs_source_release(s);
 				break;
 			}
@@ -1831,7 +1842,12 @@ void obs_enum_scenes(bool (*enum_proc)(void *, obs_source_t *), void *param)
 		source = (obs_source_t *)source->context.hh.next;
 	}
 
-	pthread_mutex_unlock(&obs->data.sources_mutex);
+	pthread_mutex_unlock(&canvas->sources_mutex);
+}
+
+void obs_enum_scenes(bool (*enum_proc)(void *, obs_source_t *), void *param)
+{
+	obs_canvas_enum_scenes(obs->data.main_canvas, enum_proc, param);
 }
 
 static inline void obs_enum(void *pstart, pthread_mutex_t *mutex, void *proc, void *param)
@@ -1895,6 +1911,22 @@ void obs_enum_services(bool (*enum_proc)(void *, obs_service_t *), void *param)
 	obs_enum(&obs->data.first_service, &obs->data.services_mutex, enum_proc, param);
 }
 
+void obs_enum_canvases(bool (*enum_proc)(void *, obs_canvas_t *), void *param)
+{
+	struct obs_context_data *start = (struct obs_context_data *)obs->data.named_canvases;
+	struct obs_context_data *context, *tmp;
+
+	pthread_mutex_lock(&obs->data.canvases_mutex);
+
+	HASH_ITER (hh, start, context, tmp) {
+		obs_canvas_t *canvas = (obs_canvas_t *)context;
+		if (!enum_proc(param, canvas))
+			break;
+	}
+
+	pthread_mutex_unlock(&obs->data.canvases_mutex);
+}
+
 static inline void *get_context_by_name(void *vfirst, const char *name, pthread_mutex_t *mutex, void *(*addref)(void *))
 {
 	struct obs_context_data **first = vfirst;
@@ -1958,9 +1990,21 @@ static inline void *obs_service_addref_safe_(void *ref)
 	return obs_service_get_ref(ref);
 }
 
+static inline void *obs_canvas_addref_safe_(void *ref)
+{
+	return obs_canvas_get_ref(ref);
+}
+
 obs_source_t *obs_get_source_by_name(const char *name)
 {
-	return get_context_by_name(&obs->data.public_sources, name, &obs->data.sources_mutex, obs_source_addref_safe_);
+	obs_source_t *source =
+		get_context_by_name(&obs->data.public_sources, name, &obs->data.sources_mutex, obs_source_addref_safe_);
+	/* For backwards compat: Also look up source name in main canvas's scenes list. */
+	if (!source) {
+		source = get_context_by_name(&obs->data.main_canvas->sources, name,
+					     &obs->data.main_canvas->sources_mutex, obs_source_addref_safe_);
+	}
+	return source;
 }
 
 obs_source_t *obs_get_source_by_uuid(const char *uuid)
@@ -1968,6 +2012,32 @@ obs_source_t *obs_get_source_by_uuid(const char *uuid)
 	return get_context_by_uuid(&obs->data.sources, uuid, &obs->data.sources_mutex, obs_source_addref_safe_);
 }
 
+obs_canvas_t *obs_get_canvas_by_name(const char *name)
+{
+	return get_context_by_name(&obs->data.named_canvases, name, &obs->data.canvases_mutex, obs_canvas_addref_safe_);
+}
+
+obs_canvas_t *obs_get_canvas_by_uuid(const char *uuid)
+{
+	return get_context_by_uuid(&obs->data.canvases, uuid, &obs->data.canvases_mutex, obs_canvas_addref_safe_);
+}
+
+obs_source_t *obs_canvas_get_source_by_name(obs_canvas_t *canvas, const char *name)
+{
+	return get_context_by_name(&canvas->sources, name, &canvas->sources_mutex, obs_source_addref_safe_);
+}
+
+obs_scene_t *obs_canvas_get_scene_by_name(obs_canvas_t *canvas, const char *name)
+{
+	obs_source_t *source = obs_canvas_get_source_by_name(canvas, name);
+	obs_scene_t *scene = obs_scene_from_source(source);
+	if (!scene) {
+		obs_source_release(source);
+		return NULL;
+	}
+	return scene;
+}
+
 obs_source_t *obs_get_transition_by_name(const char *name)
 {
 	struct obs_source **first = &obs->data.sources;
@@ -2055,16 +2125,17 @@ proc_handler_t *obs_get_proc_handler(void)
 	return obs->procs;
 }
 
-static void obs_render_main_texture_internal(enum gs_blend_type src_c, enum gs_blend_type dest_c,
-					     enum gs_blend_type src_a, enum gs_blend_type dest_a)
+static void obs_render_canvas_texture_internal(obs_canvas_t *canvas, enum gs_blend_type src_c,
+					       enum gs_blend_type dest_c, enum gs_blend_type src_a,
+					       enum gs_blend_type dest_a)
 {
 	struct obs_core_video_mix *video;
 	gs_texture_t *tex;
 	gs_effect_t *effect;
 	gs_eparam_t *param;
 
-	video = obs->video.main_mix;
-	if (!video->texture_rendered)
+	video = canvas->mix;
+	if (!video || !video->texture_rendered)
 		return;
 
 	const enum gs_color_space source_space = video->render_space;
@@ -2108,19 +2179,32 @@ static void obs_render_main_texture_internal(enum gs_blend_type src_c, enum gs_b
 
 void obs_render_main_texture(void)
 {
-	obs_render_main_texture_internal(GS_BLEND_ONE, GS_BLEND_INVSRCALPHA, GS_BLEND_ONE, GS_BLEND_INVSRCALPHA);
+	obs_render_canvas_texture_internal(obs->data.main_canvas, GS_BLEND_ONE, GS_BLEND_INVSRCALPHA, GS_BLEND_ONE,
+					   GS_BLEND_INVSRCALPHA);
 }
 
 void obs_render_main_texture_src_color_only(void)
 {
-	obs_render_main_texture_internal(GS_BLEND_ONE, GS_BLEND_ZERO, GS_BLEND_ONE, GS_BLEND_INVSRCALPHA);
+	obs_render_canvas_texture_internal(obs->data.main_canvas, GS_BLEND_ONE, GS_BLEND_ZERO, GS_BLEND_ONE,
+					   GS_BLEND_INVSRCALPHA);
+}
+
+void obs_render_canvas_texture(obs_canvas_t *canvas)
+{
+	obs_render_canvas_texture_internal(canvas, GS_BLEND_ONE, GS_BLEND_INVSRCALPHA, GS_BLEND_ONE,
+					   GS_BLEND_INVSRCALPHA);
+}
+
+void obs_render_canvas_texture_src_color_only(obs_canvas_t *canvas)
+{
+	obs_render_canvas_texture_internal(canvas, GS_BLEND_ONE, GS_BLEND_ZERO, GS_BLEND_ONE, GS_BLEND_INVSRCALPHA);
 }
 
 gs_texture_t *obs_get_main_texture(void)
 {
 	struct obs_core_video_mix *video;
 
-	video = obs->video.main_mix;
+	video = obs->data.main_canvas->mix;
 	if (!video->texture_rendered)
 		return NULL;
 
@@ -2137,6 +2221,7 @@ static obs_source_t *obs_load_source_type(obs_data_t *source_data, bool is_priva
 	const char *v_id = obs_data_get_string(source_data, "versioned_id");
 	obs_data_t *settings = obs_data_get_obj(source_data, "settings");
 	obs_data_t *hotkeys = obs_data_get_obj(source_data, "hotkeys");
+	obs_canvas_t *canvas = NULL;
 	double volume;
 	double balance;
 	int64_t sync;
@@ -2153,13 +2238,23 @@ static obs_source_t *obs_load_source_type(obs_data_t *source_data, bool is_priva
 	if (!*v_id)
 		v_id = id;
 
-	source = obs_source_create_set_last_ver(v_id, name, uuid, settings, hotkeys, prev_ver, is_private);
+	if (strcmp(id, scene_info.id) == 0 || strcmp(id, group_info.id) == 0) {
+		const char *canvas_uuid = obs_data_get_string(source_data, "canvas_uuid");
+		canvas = obs_get_canvas_by_uuid(canvas_uuid);
+		/* Fall back to main canvas if canvas cannot be found. */
+		if (!canvas) {
+			canvas = obs_canvas_get_ref(obs->data.main_canvas);
+		}
+	}
+
+	source = obs_source_create_set_last_ver(canvas, v_id, name, uuid, settings, hotkeys, prev_ver, is_private);
 
 	if (source->owns_info_id) {
 		bfree((void *)source->info.unversioned_id);
 		source->info.unversioned_id = bstrdup(id);
 	}
 
+	obs_canvas_release(canvas);
 	obs_data_release(hotkeys);
 
 	caps = obs_source_get_output_flags(source);
@@ -2322,6 +2417,7 @@ obs_data_t *obs_save_source(obs_source_t *source)
 	int m_type = (int)obs_source_get_monitoring_type(source);
 	int di_mode = (int)obs_source_get_deinterlace_mode(source);
 	int di_order = (int)obs_source_get_deinterlace_field_order(source);
+	obs_canvas_t *canvas = obs_source_get_canvas(source);
 	DARRAY(obs_source_t *) filters_copy;
 
 	obs_source_save(source);
@@ -2356,6 +2452,11 @@ obs_data_t *obs_save_source(obs_source_t *source)
 	obs_data_set_int(source_data, "deinterlace_field_order", di_order);
 	obs_data_set_int(source_data, "monitoring_type", m_type);
 
+	if (canvas) {
+		obs_data_set_string(source_data, "canvas_uuid", obs_canvas_get_uuid(canvas));
+		obs_canvas_release(canvas);
+	}
+
 	obs_data_set_obj(source_data, "private_settings", source->private_settings);
 
 	if (source->info.type == OBS_SOURCE_TYPE_TRANSITION)
@@ -2403,18 +2504,18 @@ obs_data_array_t *obs_save_sources_filtered(obs_save_source_filter_cb cb, void *
 
 	pthread_mutex_lock(&data->sources_mutex);
 
-	source = data->public_sources;
+	source = data->sources;
 
 	while (source) {
 		if ((source->info.type != OBS_SOURCE_TYPE_FILTER) != 0 && !source->removed && !source->temp_removed &&
-		    cb(data_, source)) {
+		    !source->context.private && cb(data_, source)) {
 			obs_data_t *source_data = obs_save_source(source);
 
 			obs_data_array_push_back(array, source_data);
 			obs_data_release(source_data);
 		}
 
-		source = (obs_source_t *)source->context.hh.next;
+		source = (obs_source_t *)source->context.hh_uuid.next;
 	}
 
 	pthread_mutex_unlock(&data->sources_mutex);
@@ -2499,7 +2600,7 @@ static inline bool obs_context_data_init_wrap(struct obs_context_data *context,
 	if (uuid && strlen(uuid) == UUID_STR_LENGTH)
 		context->uuid = bstrdup(uuid);
 	/* Only automatically generate UUIDs for sources */
-	else if (type == OBS_OBJ_TYPE_SOURCE)
+	else if (type == OBS_OBJ_TYPE_SOURCE || type == OBS_OBJ_TYPE_CANVAS)
 		context->uuid = os_generate_uuid();
 
 	context->name = dup_name(name, private);
@@ -2656,7 +2757,7 @@ void obs_context_data_remove(struct obs_context_data *context)
 	}
 }
 
-void obs_context_data_remove_name(struct obs_context_data *context, void *phead)
+void obs_context_data_remove_name(struct obs_context_data *context, pthread_mutex_t *mutex, void *phead)
 {
 	struct obs_context_data **head = phead;
 
@@ -2665,12 +2766,12 @@ void obs_context_data_remove_name(struct obs_context_data *context, void *phead)
 	if (!context)
 		return;
 
-	pthread_mutex_lock(context->mutex);
+	pthread_mutex_lock(mutex);
 	HASH_DELETE(hh, *head, context);
-	pthread_mutex_unlock(context->mutex);
+	pthread_mutex_unlock(mutex);
 }
 
-void obs_context_data_remove_uuid(struct obs_context_data *context, void *puuid_head)
+void obs_context_data_remove_uuid(struct obs_context_data *context, pthread_mutex_t *mutex, void *puuid_head)
 {
 	struct obs_context_data **uuid_head = puuid_head;
 
@@ -2679,9 +2780,9 @@ void obs_context_data_remove_uuid(struct obs_context_data *context, void *puuid_
 	if (!context || !context->uuid || !uuid_head)
 		return;
 
-	pthread_mutex_lock(context->mutex);
+	pthread_mutex_lock(mutex);
 	HASH_DELETE(hh_uuid, *uuid_head, context);
-	pthread_mutex_unlock(context->mutex);
+	pthread_mutex_unlock(mutex);
 }
 
 void obs_context_wait(struct obs_context_data *context)
@@ -2970,13 +3071,13 @@ void obs_add_raw_video_callback(const struct video_scale_info *conversion,
 void obs_add_raw_video_callback2(const struct video_scale_info *conversion, uint32_t frame_rate_divisor,
 				 void (*callback)(void *param, struct video_data *frame), void *param)
 {
-	struct obs_core_video_mix *video = obs->video.main_mix;
+	struct obs_core_video_mix *video = obs->data.main_canvas->mix;
 	start_raw_video(video->video, conversion, frame_rate_divisor, callback, param);
 }
 
 void obs_remove_raw_video_callback(void (*callback)(void *param, struct video_data *frame), void *param)
 {
-	struct obs_core_video_mix *video = obs->video.main_mix;
+	struct obs_core_video_mix *video = obs->data.main_canvas->mix;
 	stop_raw_video(video->video, callback, param);
 }
 
@@ -3093,13 +3194,13 @@ bool obs_video_active(void)
 
 bool obs_nv12_tex_active(void)
 {
-	struct obs_core_video_mix *video = obs->video.main_mix;
+	struct obs_core_video_mix *video = obs->data.main_canvas->mix;
 	return video->using_nv12_tex;
 }
 
 bool obs_p010_tex_active(void)
 {
-	struct obs_core_video_mix *video = obs->video.main_mix;
+	struct obs_core_video_mix *video = obs->data.main_canvas->mix;
 	return video->using_p010_tex;
 }
 
@@ -3314,3 +3415,8 @@ bool obs_enum_output_protocols(size_t idx, char **protocol)
 	*protocol = obs->data.protocols.array[idx];
 	return true;
 }
+
+obs_canvas_t *obs_get_main_canvas(void)
+{
+	return obs_canvas_get_ref(obs->data.main_canvas);
+}

+ 114 - 0
libobs/obs.h

@@ -51,6 +51,7 @@ struct obs_service;
 struct obs_module;
 struct obs_fader;
 struct obs_volmeter;
+struct obs_canvas;
 
 typedef struct obs_context_data obs_object_t;
 typedef struct obs_display obs_display_t;
@@ -65,12 +66,14 @@ typedef struct obs_service obs_service_t;
 typedef struct obs_module obs_module_t;
 typedef struct obs_fader obs_fader_t;
 typedef struct obs_volmeter obs_volmeter_t;
+typedef struct obs_canvas obs_canvas_t;
 
 typedef struct obs_weak_object obs_weak_object_t;
 typedef struct obs_weak_source obs_weak_source_t;
 typedef struct obs_weak_output obs_weak_output_t;
 typedef struct obs_weak_encoder obs_weak_encoder_t;
 typedef struct obs_weak_service obs_weak_service_t;
+typedef struct obs_weak_canvas obs_weak_canvas_t;
 
 #include "obs-missing-files.h"
 #include "obs-source.h"
@@ -688,6 +691,9 @@ EXPORT void obs_enum_encoders(bool (*enum_proc)(void *, obs_encoder_t *), void *
 /** Enumerates encoders */
 EXPORT void obs_enum_services(bool (*enum_proc)(void *, obs_service_t *), void *param);
 
+/** Enumerates canvases */
+EXPORT void obs_enum_canvases(bool (*enum_proc)(void *, obs_canvas_t *), void *param);
+
 /**
  * Gets a source by its name.
  *
@@ -719,6 +725,11 @@ EXPORT obs_encoder_t *obs_get_encoder_by_name(const char *name);
 /** Gets an service by its name. */
 EXPORT obs_service_t *obs_get_service_by_name(const char *name);
 
+/** Get a canvas by its name. */
+EXPORT obs_canvas_t *obs_get_canvas_by_name(const char *name);
+/** Get a canvas by its UUID. */
+EXPORT obs_canvas_t *obs_get_canvas_by_uuid(const char *uuid);
+
 enum obs_base_effect {
 	OBS_EFFECT_DEFAULT,             /**< RGB/YUV */
 	OBS_EFFECT_DEFAULT_RECT,        /**< RGB/YUV (using texture_rect) */
@@ -747,6 +758,12 @@ EXPORT void obs_render_main_texture(void);
 /** Renders the last main output texture ignoring background color */
 EXPORT void obs_render_main_texture_src_color_only(void);
 
+/** Renders the last canvas output texture */
+EXPORT void obs_render_canvas_texture(obs_canvas_t *canvas);
+
+/** Renders the last main output texture ignoring background color */
+EXPORT void obs_render_canvas_texture_src_color_only(obs_canvas_t *canvas);
+
 /** Returns the last main output texture.  This can return NULL if the texture
  * is unavailable. */
 EXPORT gs_texture_t *obs_get_main_texture(void);
@@ -790,6 +807,7 @@ enum obs_obj_type {
 	OBS_OBJ_TYPE_OUTPUT,
 	OBS_OBJ_TYPE_ENCODER,
 	OBS_OBJ_TYPE_SERVICE,
+	OBS_OBJ_TYPE_CANVAS,
 };
 
 EXPORT enum obs_obj_type obs_obj_get_type(void *obj);
@@ -1482,6 +1500,9 @@ EXPORT enum obs_media_state obs_source_media_get_state(obs_source_t *source);
 EXPORT void obs_source_media_started(obs_source_t *source);
 EXPORT void obs_source_media_ended(obs_source_t *source);
 
+/** Get canvas this source belongs to (reference incremented) */
+EXPORT obs_canvas_t *obs_source_get_canvas(const obs_source_t *source);
+
 /* ------------------------------------------------------------------------- */
 /* Transition-specific functions */
 enum obs_transition_target {
@@ -2484,6 +2505,99 @@ EXPORT void obs_source_frame_copy(struct obs_source_frame *dst, const struct obs
 /* Get source icon type */
 EXPORT enum obs_icon_type obs_source_get_icon_type(const char *id);
 
+/* ------------------------------------------------------------------------- */
+/* Canvases */
+
+/* Canvas flags */
+enum obs_canvas_flags {
+	MAIN = 1 << 0,      // Main canvas created by libobs, cannot be renamed or reset, cannot be set by user
+	ACTIVATE = 1 << 1,  // Canvas sources will become active when they are visible
+	MIX_AUDIO = 1 << 2, // Audio from channels in this canvas will be mixed into the audio output
+	SCENE_REF = 1 << 3, // Canvas will hold references for scene sources
+	EPHEMERAL = 1 << 4, // Indicates this canvas is not supposed to be saved
+
+	/* Presets */
+	PROGRAM = ACTIVATE | MIX_AUDIO | SCENE_REF,
+	PREVIEW = EPHEMERAL,
+	DEVICE = ACTIVATE | EPHEMERAL,
+};
+
+/** Get a strong reference to the main OBS canvas */
+EXPORT obs_canvas_t *obs_get_main_canvas(void);
+
+/** Creates a new canvas */
+EXPORT obs_canvas_t *obs_canvas_create(const char *name, struct obs_video_info *ovi, uint32_t flags);
+/** Creates a new private canvas */
+EXPORT obs_canvas_t *obs_canvas_create_private(const char *name, struct obs_video_info *ovi, uint32_t flags);
+
+/** Signal that references to canvas should be released and mark the canvas as removed. */
+EXPORT void obs_canvas_remove(obs_canvas_t *canvas);
+/** Returns if a canvas is marked as removed (i.e., should no longer be used). */
+EXPORT bool obs_canvas_removed(obs_canvas_t *canvas);
+
+/* Canvas properties */
+/** Set canvas name */
+EXPORT void obs_canvas_set_name(obs_canvas_t *canvas, const char *name);
+/** Get canvas name */
+EXPORT const char *obs_canvas_get_name(const obs_canvas_t *canvas);
+/** Get canvas UUID */
+EXPORT const char *obs_canvas_get_uuid(const obs_canvas_t *canvas);
+/** Gets flags set on a canvas */
+EXPORT uint32_t obs_canvas_get_flags(const obs_canvas_t *canvas);
+
+/* Saving/Loading */
+/** Saves a canvas to settings data */
+EXPORT obs_data_t *obs_save_canvas(obs_canvas_t *source);
+/** Loads a canvas from settings data */
+EXPORT obs_canvas_t *obs_load_canvas(obs_data_t *data);
+
+/* Reference counting */
+/** Add strong reference */
+EXPORT obs_canvas_t *obs_canvas_get_ref(obs_canvas_t *canvas);
+/** Release strong reference */
+EXPORT void obs_canvas_release(obs_canvas_t *canvas);
+/** Add weak reference */
+EXPORT void obs_weak_canvas_addref(obs_weak_canvas_t *weak);
+/** Release weak reference */
+EXPORT void obs_weak_canvas_release(obs_weak_canvas_t *weak);
+
+/** Get weak reference from strong reference */
+EXPORT obs_weak_canvas_t *obs_canvas_get_weak_canvas(obs_canvas_t *canvas);
+/** Get strong reference from weak reference */
+EXPORT obs_canvas_t *obs_weak_canvas_get_canvas(obs_weak_canvas_t *weak);
+
+/* Channels */
+/** Sets the source to be used for this canvas. */
+EXPORT void obs_canvas_set_channel(obs_canvas_t *canvas, uint32_t channel, obs_source_t *source);
+/** Gets the source currently in use for this view context */
+EXPORT obs_source_t *obs_canvas_get_channel(obs_canvas_t *canvas, uint32_t channel);
+
+/* Canvas sources */
+/** Create scene attached to a canvas */
+EXPORT obs_scene_t *obs_canvas_scene_create(obs_canvas_t *canvas, const char *name);
+/** Remove a scene from a canvas */
+EXPORT void obs_canvas_scene_remove(obs_scene_t *scene);
+/** Move scene to another canvas, detaching it from the previous one and deduplicating the name if needed */
+EXPORT void obs_canvas_move_scene(obs_scene_t *scene, obs_canvas_t *dst);
+/** Enumerates scenes belonging to a canvas */
+EXPORT void obs_canvas_enum_scenes(obs_canvas_t *canvas, bool (*enum_proc)(void *, obs_source_t *), void *param);
+/** Get a canvas source by name */
+EXPORT obs_source_t *obs_canvas_get_source_by_name(obs_canvas_t *canvas, const char *name);
+/** Get a canvas source by UUID */
+EXPORT obs_scene_t *obs_canvas_get_scene_by_name(obs_canvas_t *canvas, const char *name);
+
+/* Canvas video */
+/** Reset a canvas's video mix */
+EXPORT bool obs_canvas_reset_video(obs_canvas_t *canvas, struct obs_video_info *ovi);
+/** Returns true if the canvas video is configured */
+EXPORT bool obs_canvas_has_video(obs_canvas_t *canvas);
+/** Get canvas video output */
+EXPORT video_t *obs_canvas_get_video(const obs_canvas_t *canvas);
+/** Get canvas video info (if it exists) */
+EXPORT bool obs_canvas_get_video_info(const obs_canvas_t *canvas, struct obs_video_info *ovi);
+/** Renders the sources of this canvas's view context */
+EXPORT void obs_canvas_render(obs_canvas_t *canvas);
+
 #ifdef __cplusplus
 }
 #endif