--[[ ** ** clone-template-scene.lua -- OBS Studio Lua Script for Cloning Template Scene ** Copyright (c) 2021-2022 Dr. Ralf S. Engelschall <rse@engelschall.com> ** Distributed under MIT license <https://spdx.org/licenses/MIT.html> ** --]] -- 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 [[ <h2>Clone Template Scene</h2> Copyright © 2021-2022 <a style="color: #ffffff; text-decoration: none;" href="http://engelschall.com">Dr. Ralf S. Engelschall</a><br/> Distributed under <a style="color: #ffffff; text-decoration: none;" href="https://spdx.org/licenses/MIT.html">MIT license</a> <p> <b>Clone an entire source scene (template), by creating a target scene (clone) and copying all corresponding sources, including their filters, transforms, etc.</b> <p> <u>Notice:</u> The same kind of cloning <i>cannot</i> to be achieved manually, as the scene <i>Duplicate</i> and the source <i>Copy</i> 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. <p> <u>Prerequisite:</u> This script assumes that the source scene is named <tt>XXX</tt> (e.g. <tt>Template-01</tt>), all of its sources are named <tt>XXX-ZZZ</tt> (e.g. <tt>Template-01-Placeholder-02</tt>), the target scene is named <tt>YYY</tt> (e.g. <tt>Scene-03</tt>) and all of its sources are consequently named <tt>YYY-ZZZ</tt> (e.g. <tt>Scene-03-Placeholder-02</tt>). ]] 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