| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522 | #include <obs-module.h>#include <obs-frontend-api.h>#include <QMainWindow>#include <QAction>#include <util/util.hpp>#include <util/platform.h>#include <media-io/video-io.h>#include <media-io/video-frame.h>#include "DecklinkOutputUI.h"#include "../../../plugins/decklink/const.h"OBS_DECLARE_MODULE()OBS_MODULE_USE_DEFAULT_LOCALE("decklink-output-ui", "en-US")DecklinkOutputUI *doUI;bool shutting_down = false;bool main_output_running = false;bool preview_output_running = false;constexpr size_t STAGE_BUFFER_COUNT = 3;struct decklink_ui_output {	bool enabled;	obs_source_t *current_source;	obs_output_t *output;	video_t *video_queue;	gs_texrender_t *texrender_premultiplied;	gs_texrender_t *texrender;	gs_stagesurf_t *stagesurfaces[STAGE_BUFFER_COUNT];	bool surf_written[STAGE_BUFFER_COUNT];	size_t stage_index;	uint8_t *video_data;	uint32_t video_linesize;	obs_video_info ovi;};static struct decklink_ui_output context = {0};static struct decklink_ui_output context_preview = {0};OBSData load_settings(){	BPtr<char> path = obs_module_get_config_path(		obs_current_module(), "decklinkOutputProps.json");	BPtr<char> jsonData = os_quick_read_utf8_file(path);	if (!!jsonData) {		obs_data_t *data = obs_data_create_from_json(jsonData);		OBSData dataRet(data);		obs_data_release(data);		return dataRet;	}	return nullptr;}static void decklink_ui_tick(void *param, float sec);static void decklink_ui_render(void *param);void output_stop(){	obs_remove_main_rendered_callback(decklink_ui_render, &context);	obs_output_stop(context.output);	obs_output_release(context.output);	obs_enter_graphics();	for (gs_stagesurf_t *&surf : context.stagesurfaces) {		gs_stagesurface_destroy(surf);		surf = nullptr;	}	gs_texrender_destroy(context.texrender);	context.texrender = nullptr;	obs_leave_graphics();	video_output_close(context.video_queue);	obs_remove_tick_callback(decklink_ui_tick, &context);	main_output_running = false;	if (!shutting_down)		doUI->OutputStateChanged(false);}void output_start(){	OBSData settings = load_settings();	if (settings != nullptr) {		obs_output_t *const output = obs_output_create(			"decklink_output", "decklink_output", settings, NULL);		const struct video_scale_info *const conversion =			obs_output_get_video_conversion(output);		if (conversion != nullptr) {			context.output = output;			obs_add_tick_callback(decklink_ui_tick, &context);			obs_get_video_info(&context.ovi);			const uint32_t width = conversion->width;			const uint32_t height = conversion->height;			obs_enter_graphics();			context.texrender_premultiplied = nullptr;			context.texrender =				gs_texrender_create(GS_BGRA, GS_ZS_NONE);			for (gs_stagesurf_t *&surf : context.stagesurfaces)				surf = gs_stagesurface_create(width, height,							      GS_BGRA);			obs_leave_graphics();			for (bool &written : context.surf_written)				written = false;			context.stage_index = 0;			video_output_info vi = {0};			vi.format = VIDEO_FORMAT_BGRA;			vi.width = width;			vi.height = height;			vi.fps_den = context.ovi.fps_den;			vi.fps_num = context.ovi.fps_num;			vi.cache_size = 16;			vi.colorspace = VIDEO_CS_DEFAULT;			vi.range = VIDEO_RANGE_FULL;			vi.name = "decklink_output";			video_output_open(&context.video_queue, &vi);			context.current_source = nullptr;			obs_add_main_rendered_callback(decklink_ui_render,						       &context);			obs_output_set_media(context.output,					     context.video_queue,					     obs_get_audio());			bool started = obs_output_start(context.output);			main_output_running = started;			if (!shutting_down)				doUI->OutputStateChanged(started);			if (!started)				output_stop();		} else {			obs_output_release(output);		}	}}void output_toggle(){	if (main_output_running)		output_stop();	else		output_start();}OBSData load_preview_settings(){	BPtr<char> path = obs_module_get_config_path(		obs_current_module(), "decklinkPreviewOutputProps.json");	BPtr<char> jsonData = os_quick_read_utf8_file(path);	if (!!jsonData) {		obs_data_t *data = obs_data_create_from_json(jsonData);		OBSData dataRet(data);		obs_data_release(data);		return dataRet;	}	return nullptr;}void on_preview_scene_changed(enum obs_frontend_event event, void *param);static void decklink_ui_tick(void *param, float /* sec */){	auto ctx = (struct decklink_ui_output *)param;	if (ctx->texrender_premultiplied)		gs_texrender_reset(ctx->texrender_premultiplied);	if (ctx->texrender)		gs_texrender_reset(ctx->texrender);}void preview_output_stop(){	obs_remove_main_rendered_callback(decklink_ui_render, &context_preview);	obs_frontend_remove_event_callback(on_preview_scene_changed,					   &context_preview);	obs_output_stop(context_preview.output);	obs_output_release(context_preview.output);	obs_source_release(context_preview.current_source);	obs_enter_graphics();	for (gs_stagesurf_t *&surf : context_preview.stagesurfaces) {		gs_stagesurface_destroy(surf);		surf = nullptr;	}	gs_texrender_destroy(context_preview.texrender);	context_preview.texrender = nullptr;	gs_texrender_destroy(context_preview.texrender_premultiplied);	context_preview.texrender_premultiplied = nullptr;	obs_leave_graphics();	video_output_close(context_preview.video_queue);	obs_remove_tick_callback(decklink_ui_tick, &context_preview);	preview_output_running = false;	if (!shutting_down)		doUI->PreviewOutputStateChanged(false);}void preview_output_start(){	OBSData settings = load_preview_settings();	if (settings != nullptr) {		obs_output_t *const output = obs_output_create(			"decklink_output", "decklink_output", settings, NULL);		const struct video_scale_info *const conversion =			obs_output_get_video_conversion(output);		if (conversion != nullptr) {			context_preview.output = output;			obs_add_tick_callback(decklink_ui_tick,					      &context_preview);			obs_get_video_info(&context_preview.ovi);			const uint32_t width = conversion->width;			const uint32_t height = conversion->height;			obs_enter_graphics();			context_preview.texrender_premultiplied =				gs_texrender_create(GS_BGRA, GS_ZS_NONE);			context_preview.texrender =				gs_texrender_create(GS_BGRA, GS_ZS_NONE);			for (gs_stagesurf_t *&surf :			     context_preview.stagesurfaces)				surf = gs_stagesurface_create(width, height,							      GS_BGRA);			obs_leave_graphics();			for (bool &written : context_preview.surf_written)				written = false;			context_preview.stage_index = 0;			video_output_info vi = {0};			vi.format = VIDEO_FORMAT_BGRA;			vi.width = width;			vi.height = height;			vi.fps_den = context_preview.ovi.fps_den;			vi.fps_num = context_preview.ovi.fps_num;			vi.cache_size = 16;			vi.colorspace = VIDEO_CS_DEFAULT;			vi.range = VIDEO_RANGE_FULL;			vi.name = "decklink_preview_output";			video_output_open(&context_preview.video_queue, &vi);			obs_frontend_add_event_callback(				on_preview_scene_changed, &context_preview);			if (obs_frontend_preview_program_mode_active()) {				context_preview.current_source =					obs_frontend_get_current_preview_scene();			} else {				context_preview.current_source =					obs_frontend_get_current_scene();			}			obs_add_main_rendered_callback(decklink_ui_render,						       &context_preview);			obs_output_set_media(context_preview.output,					     context_preview.video_queue,					     obs_get_audio());			bool started = obs_output_start(context_preview.output);			preview_output_running = started;			if (!shutting_down)				doUI->PreviewOutputStateChanged(started);			if (!started)				preview_output_stop();		} else {			obs_output_release(output);		}	}}void preview_output_toggle(){	if (preview_output_running)		preview_output_stop();	else		preview_output_start();}void on_preview_scene_changed(enum obs_frontend_event event, void *param){	auto ctx = (struct decklink_ui_output *)param;	switch (event) {	case OBS_FRONTEND_EVENT_STUDIO_MODE_ENABLED:	case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED:		obs_source_release(ctx->current_source);		ctx->current_source = obs_frontend_get_current_preview_scene();		break;	case OBS_FRONTEND_EVENT_STUDIO_MODE_DISABLED:		obs_source_release(ctx->current_source);		ctx->current_source = obs_frontend_get_current_scene();		break;	case OBS_FRONTEND_EVENT_SCENE_CHANGED:		if (!obs_frontend_preview_program_mode_active()) {			obs_source_release(ctx->current_source);			ctx->current_source = obs_frontend_get_current_scene();		}		break;	default:		break;	}}static void decklink_ui_render(void *param){	auto *const ctx = (struct decklink_ui_output *)param;	uint32_t width = 0;	uint32_t height = 0;	gs_texture_t *tex = nullptr;	if (ctx == &context) {		if (!main_output_running)			return;		tex = obs_get_main_texture();		if (!tex)			return;		width = gs_texture_get_width(tex);		height = gs_texture_get_height(tex);	} else if (ctx == &context_preview) {		if (!preview_output_running)			return;		if (!ctx->current_source)			return;		width = obs_source_get_base_width(ctx->current_source);		height = obs_source_get_base_height(ctx->current_source);		gs_texrender_t *const texrender_premultiplied =			ctx->texrender_premultiplied;		if (!gs_texrender_begin(texrender_premultiplied, width, height))			return;		struct vec4 background;		vec4_zero(&background);		gs_clear(GS_CLEAR_COLOR, &background, 0.0f, 0);		gs_ortho(0.0f, (float)width, 0.0f, (float)height, -100.0f,			 100.0f);		gs_blend_state_push();		gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO);		obs_source_video_render(ctx->current_source);		gs_blend_state_pop();		gs_texrender_end(texrender_premultiplied);		tex = gs_texrender_get_texture(texrender_premultiplied);	} else {		return;	}	const struct video_scale_info *const conversion =		obs_output_get_video_conversion(ctx->output);	const uint32_t scaled_width = conversion->width;	const uint32_t scaled_height = conversion->height;	if (!gs_texrender_begin(ctx->texrender, scaled_width, scaled_height))		return;	const bool previous = gs_framebuffer_srgb_enabled();	const bool source_hdr = (ctx->ovi.colorspace == VIDEO_CS_2100_PQ) ||				(ctx->ovi.colorspace == VIDEO_CS_2100_HLG);	const bool target_hdr = source_hdr &&				(conversion->colorspace == VIDEO_CS_2100_PQ);	gs_enable_framebuffer_srgb(!target_hdr);	gs_enable_blending(false);	gs_effect_t *const effect = obs_get_base_effect(OBS_EFFECT_DEFAULT);	gs_effect_set_texture_srgb(gs_effect_get_param_by_name(effect, "image"),				   tex);	const char *const tech_name =		target_hdr ? "DrawAlphaDivideR10L"			   : (source_hdr ? "DrawAlphaDivideTonemap"					 : "DrawAlphaDivide");	while (gs_effect_loop(effect, tech_name)) {		gs_effect_set_float(gs_effect_get_param_by_name(effect,								"multiplier"),				    obs_get_video_sdr_white_level() / 10000.f);		gs_draw_sprite(tex, 0, 0, 0);	}	gs_enable_blending(true);	gs_enable_framebuffer_srgb(previous);	gs_texrender_end(ctx->texrender);	const size_t write_stage_index = ctx->stage_index;	gs_stage_texture(ctx->stagesurfaces[write_stage_index],			 gs_texrender_get_texture(ctx->texrender));	ctx->surf_written[write_stage_index] = true;	const size_t read_stage_index =		(write_stage_index + 1) % STAGE_BUFFER_COUNT;	if (ctx->surf_written[read_stage_index]) {		struct video_frame output_frame;		if (video_output_lock_frame(ctx->video_queue, &output_frame, 1,					    os_gettime_ns())) {			gs_stagesurf_t *const read_surf =				ctx->stagesurfaces[read_stage_index];			if (gs_stagesurface_map(read_surf, &ctx->video_data,						&ctx->video_linesize)) {				uint32_t linesize = output_frame.linesize[0];				for (uint32_t i = 0; i < scaled_height; i++) {					uint32_t dst_offset = linesize * i;					uint32_t src_offset =						ctx->video_linesize * i;					memcpy(output_frame.data[0] +						       dst_offset,					       ctx->video_data + src_offset,					       linesize);				}				gs_stagesurface_unmap(read_surf);				ctx->video_data = nullptr;			}			video_output_unlock_frame(ctx->video_queue);		}	}	ctx->stage_index = read_stage_index;}void addOutputUI(void){	QAction *action = (QAction *)obs_frontend_add_tools_menu_qaction(		obs_module_text("Decklink Output"));	QMainWindow *window = (QMainWindow *)obs_frontend_get_main_window();	obs_frontend_push_ui_translation(obs_module_get_string);	doUI = new DecklinkOutputUI(window);	obs_frontend_pop_ui_translation();	auto cb = []() {		doUI->ShowHideDialog();	};	action->connect(action, &QAction::triggered, cb);}static void OBSEvent(enum obs_frontend_event event, void *){	if (event == OBS_FRONTEND_EVENT_FINISHED_LOADING) {		OBSData settings = load_settings();		if (settings && obs_data_get_bool(settings, "auto_start"))			output_start();		OBSData previewSettings = load_preview_settings();		if (previewSettings &&		    obs_data_get_bool(previewSettings, "auto_start"))			preview_output_start();	} else if (event == OBS_FRONTEND_EVENT_EXIT) {		shutting_down = true;		if (preview_output_running)			preview_output_stop();		if (main_output_running)			output_stop();	}}bool obs_module_load(void){	return true;}void obs_module_unload(void){	shutting_down = true;	if (preview_output_running)		preview_output_stop();	if (main_output_running)		output_stop();}void obs_module_post_load(void){	if (!obs_get_module("decklink"))		return;	addOutputUI();	obs_frontend_add_event_callback(OBSEvent, nullptr);}
 |