--[[ ** ** clone-template-scene.lua -- OBS Studio Lua Script for Cloning Template Scene ** Copyright (c) 2021-2022 Dr. Ralf S. Engelschall ** Distributed under MIT license ** --]] -- global OBS API local obs = obslua -- global context information local ctx = { propsDef = nil, -- property definition propsDefSrc = nil, -- property definition (source scene) propsSet = nil, -- property settings (model) propsVal = {}, -- property values propsValSrc = nil, -- property values (first source scene) } -- helper function: set status message local function statusMessage (type, message) if type == "error" then obs.script_log(obs.LOG_INFO, message) obs.obs_data_set_string(ctx.propsSet, "statusMessage", string.format("ERROR: %s", message)) else obs.script_log(obs.LOG_INFO, message) obs.obs_data_set_string(ctx.propsSet, "statusMessage", string.format("INFO: %s", message)) end obs.obs_properties_apply_settings(ctx.propsDef, ctx.propsSet) return true end -- helper function: find scene by name local function findSceneByName (name) local scenes = obs.obs_frontend_get_scenes() if scenes == nil then return nil end for _, scene in ipairs(scenes) do local n = obs.obs_source_get_name(scene) if n == name then obs.source_list_release(scenes) return scene end end obs.source_list_release(scenes) return nil end -- helper function: replace a string local function stringReplace (str, from, to) local function regexEscape (s) return string.gsub(s, "[%(%)%.%%%+%-%*%?%[%^%$%]]", "%%%1") end return string.gsub(str, regexEscape(from), to) end -- called for the actual cloning action local function doClone () -- find source scene (template) local sourceScene = findSceneByName(ctx.propsVal.sourceScene) if sourceScene == nil then statusMessage("error", string.format("source scene \"%s\" not found!", ctx.propsVal.sourceScene)) return true end -- find target scene (clone) local targetScene = findSceneByName(ctx.propsVal.targetScene) if targetScene ~= nil then statusMessage("error", string.format("target scene \"%s\" already exists!", ctx.propsVal.targetScene)) return true end -- create target scene obs.script_log(obs.LOG_INFO, string.format("create: SCENE \"%s\"", ctx.propsVal.targetScene)) targetScene = obs.obs_scene_create(ctx.propsVal.targetScene) -- iterate over all source scene (template) sources local sourceSceneBase = obs.obs_scene_from_source(sourceScene) local sourceItems = obs.obs_scene_enum_items(sourceSceneBase) for _, sourceItem in ipairs(sourceItems) do local sourceSrc = obs.obs_sceneitem_get_source(sourceItem) -- determine source and destination name local sourceNameSrc = obs.obs_source_get_name(sourceSrc) local sourceNameDst = stringReplace(sourceNameSrc, ctx.propsVal.sourceScene, ctx.propsVal.targetScene) obs.script_log(obs.LOG_INFO, string.format("create: SOURCE \"%s/%s\"", ctx.propsVal.targetScene, sourceNameDst)) -- create source local type = obs.obs_source_get_id(sourceSrc) local settings = obs.obs_source_get_settings(sourceSrc) local targetSource = obs.obs_source_create(type, sourceNameDst, settings, nil) -- add source to scene local targetItem = obs.obs_scene_add(targetScene, targetSource) -- copy source private settings local privSettings = obs.obs_source_get_private_settings(sourceSrc) local hidden = obs.obs_data_get_bool(privSettings, "mixer_hidden") local volumeLocked = obs.obs_data_get_bool(privSettings, "volume_locked") local showInMultiview = obs.obs_data_get_bool(privSettings, "show_in_multiview") obs.obs_data_release(privSettings) privSettings = obs.obs_source_get_private_settings(targetSource) obs.obs_data_set_bool(privSettings, "mixer_hidden", hidden) obs.obs_data_set_bool(privSettings, "volume_locked", volumeLocked) obs.obs_data_set_bool(privSettings, "show_in_multiview", showInMultiview) obs.obs_data_release(privSettings) -- copy source transforms local transform = obs.obs_transform_info() obs.obs_sceneitem_get_info(sourceItem, transform) obs.obs_sceneitem_set_info(targetItem, transform) -- copy source crop local crop = obs.obs_sceneitem_crop() obs.obs_sceneitem_get_crop(sourceItem, crop) obs.obs_sceneitem_set_crop(targetItem, crop) -- copy source filters obs.obs_source_copy_filters(targetSource, sourceSrc) -- copy source volume local volume = obs.obs_source_get_volume(sourceSrc) obs.obs_source_set_volume(targetSource, volume) -- copy source muted state local muted = obs.obs_source_muted(sourceSrc) obs.obs_source_set_muted(targetSource, muted) -- copy source push-to-mute state local pushToMute = obs.obs_source_push_to_mute_enabled(sourceSrc) obs.obs_source_enable_push_to_mute(targetSource, pushToMute) -- copy source push-to-mute delay local pushToMuteDelay = obs.obs_source_get_push_to_mute_delay(sourceSrc) obs.obs_source_set_push_to_mute_delay(targetSource, pushToMuteDelay) -- copy source push-to-talk state local pushToTalk = obs.obs_source_push_to_talk_enabled(sourceSrc) obs.obs_source_enable_push_to_talk(targetSource, pushToTalk) -- copy source push-to-talk delay local pushToTalkDelay = obs.obs_source_get_push_to_talk_delay(sourceSrc) obs.obs_source_set_push_to_talk_delay(targetSource, pushToTalkDelay) -- copy source sync offset local offset = obs.obs_source_get_sync_offset(sourceSrc) obs.obs_source_set_sync_offset(targetSource, offset) -- copy source mixer state local mixers = obs.obs_source_get_audio_mixers(sourceSrc) obs.obs_source_set_audio_mixers(targetSource, mixers) -- copy source deinterlace mode local mode = obs.obs_source_get_deinterlace_mode(sourceSrc) obs.obs_source_set_deinterlace_mode(targetSource, mode) -- copy source deinterlace field order local fieldOrder = obs.obs_source_get_deinterlace_field_order(sourceSrc) obs.obs_source_set_deinterlace_field_order(targetSource, fieldOrder) -- copy source flags local flags = obs.obs_source_get_flags(sourceSrc) obs.obs_source_set_flags(targetSource, flags) -- copy source enabled state local enabled = obs.obs_source_enabled(sourceSrc) obs.obs_source_set_enabled(targetSource, enabled) -- copy source visible state local visible = obs.obs_sceneitem_visible(sourceItem) obs.obs_sceneitem_set_visible(targetItem, visible) -- copy source locked state local locked = obs.obs_sceneitem_locked(sourceItem) obs.obs_sceneitem_set_locked(targetItem, locked) -- release resources obs.obs_source_release(targetSource) obs.obs_data_release(settings) end -- release resources obs.sceneitem_list_release(sourceItems) obs.obs_scene_release(targetScene) -- final hint statusMessage("info", string.format("scene \"%s\" successfully cloned to \"%s\".", ctx.propsVal.sourceScene, ctx.propsVal.targetScene)) return true end -- helper function: update source scenes property local function updateSourceScenes () if ctx.propsDefSrc == nil then return end obs.obs_property_list_clear(ctx.propsDefSrc) local scenes = obs.obs_frontend_get_scenes() if scenes == nil then return end ctx.propsValSrc = nil for _, scene in ipairs(scenes) do local n = obs.obs_source_get_name(scene) obs.obs_property_list_add_string(ctx.propsDefSrc, n, n) ctx.propsValSrc = n end obs.source_list_release(scenes) end -- script hook: description displayed on script window function script_description () return [[

Clone Template Scene

Copyright © 2021-2022 Dr. Ralf S. Engelschall
Distributed under MIT license

Clone an entire source scene (template), by creating a target scene (clone) and copying all corresponding sources, including their filters, transforms, etc.

Notice: The same kind of cloning cannot to be achieved manually, as the scene Duplicate and the source Copy functions create references for many source types only and especially do not clone applied transforms. The only alternative is the tedious process of creating a new scene, step-by-step copying and pasting all sources and then also step-by-step copying and pasting all source transforms.

Prerequisite: This script assumes that the source scene is named XXX (e.g. Template-01), all of its sources are named XXX-ZZZ (e.g. Template-01-Placeholder-02), the target scene is named YYY (e.g. Scene-03) and all of its sources are consequently named YYY-ZZZ (e.g. Scene-03-Placeholder-02). ]] end -- script hook: define UI properties function script_properties () -- create new properties ctx.propsDef = obs.obs_properties_create() -- create source scene list ctx.propsDefSrc = obs.obs_properties_add_list(ctx.propsDef, "sourceScene", "Source Scene (Template):", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING) updateSourceScenes() -- create target scene field obs.obs_properties_add_text(ctx.propsDef, "targetScene", "Target Scene (Clone):", obs.OBS_TEXT_DEFAULT) -- create clone button obs.obs_properties_add_button(ctx.propsDef, "clone", "Clone Template Scene", doClone) -- create status field (read-only) local status = obs.obs_properties_add_text(ctx.propsDef, "statusMessage", "Status Message:", obs.OBS_TEXT_MULTILINE) obs.obs_property_set_enabled(status, false) -- apply values to definitions obs.obs_properties_apply_settings(ctx.propsDef, ctx.propsSet) return ctx.propsDef end -- script hook: define property defaults function script_defaults (settings) -- update our source scene list (for propsValSrc below) updateSourceScenes() -- provide default values obs.obs_data_set_default_string(settings, "sourceScene", ctx.propsValSrc) obs.obs_data_set_default_string(settings, "targetScene", "Scene-01") obs.obs_data_set_default_string(settings, "statusMessage", "") end -- script hook: property values were updated function script_update (settings) -- remember settings ctx.propsSet = settings -- fetch property values ctx.propsVal.sourceScene = obs.obs_data_get_string(settings, "sourceScene") ctx.propsVal.targetScene = obs.obs_data_get_string(settings, "targetScene") ctx.propsVal.statusMessage = obs.obs_data_get_string(settings, "statusMessage") end -- react on script load function script_load (settings) -- clear status message obs.obs_data_set_string(settings, "statusMessage", "") -- react on scene list changes obs.obs_frontend_add_event_callback(function (event) if event == obs.OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED then -- update our source scene list updateSourceScenes() end return true end) end