------------------------------------------------------- -- SceneShow by W. Zaggle (DCStrato) Ver 1.01 Oct 2023 ------------------------------------------------------- -- Updates 10/13/2023 Correction to properly update valid scenes when scene collection changes obs = obslua ------------------------------------------------------------------------------------------------------------------ -- Global Variables ------------------------------------------------------------------------------------------------------------------ script_sets = nil -- Script Settings holder script_props = nil -- Script Properties holder slide_fade_speed = 0 -- Time between Slides prefixString = "" -- Current slide show Prefix previousSlide = false -- Flag to signal previous slide hotkey (reverse slide direction) useAutoReading = false -- Flag to indicate initial auto or manual slide advance skip bad read (OBS BUG WORKAROUND) useAutoTiming = false -- Flag to indicate auto or manual slide advance nextSlide = false -- Flag to signal next slide hotkey useCustomList = false -- Flag to indicate the custom scene list SHOULD be used (alternative is to empty the list) usingCustomList = false -- Flag to indicate a custom scene list is being used useLooping = false -- Flag to indicate slide show should restart after last slide showhelp = false -- Flag to indicate help is showing or hidden showSceneList = {} -- Working Array of the Existing Custom Scenes List validScenesList = {} -- Working Array of Existing Scenes that can be included in custom list (easier to index into for validity testing) ------------------------------------------------------------------------------------------------------------------ -- This routine does most of the work by finding the first, last, next and previous scenes either -- from the specified Prefix, or from the Custom Group list, and transitioning to that new scene ------------------------------------------------------------------------------------------------------------------ function advance_slideScene() obs.remove_current_callback() -- stop any pending additional callbacks local scene = obs.obs_frontend_get_current_scene() local current_scene = obs.obs_source_get_name(scene) local scenes = obs.obs_frontend_get_scenes() obs.obs_source_release(scene) local nextSceneNum = nil local sceneNum = nil local nextSceneName = nil if usingCustomList == true then -- find next scene in list local scenes = obs.obs_frontend_get_scenes() local sceneList = obs.obs_properties_get(script_props, "prop_prepared_list") if #showSceneList > 0 then for i = 1, #showSceneList do if showSceneList[i] == current_scene then -- found current scene in list if previousSlide then if i > 1 then nextSceneName = showSceneList[i - 1] else if useLooping then nextSceneName = showSceneList[#showSceneList] -- loop back to end of list end end else if i < #showSceneList then nextSceneName = showSceneList[i + 1] -- use next scene in list else if useLooping then nextSceneName = showSceneList[1] -- loop back to start of list end end end break end end end else local _, _, sceneNum = current_scene:find("(%d+)") if sceneNum ~= nil then local sceneNumber = tonumber(sceneNum) local lastScene = 0 local lastSceneName = nil local firstScene = 10000 local firstSceneName = nil local forwardScene = 10000 local forwardSceneName = nil local reverseScene = 0 local reverseSceneName = nil if scenes ~= nil then for i, scn in ipairs(scenes) do --look at all scenes local sceneName = obs.obs_source_get_name(scn) local sceneIndex, _, sceneNS = sceneName:find(prefixString .. "(%d+)") -- Get scene number of scene if it exists if sceneNS ~= nil and sceneIndex == 1 then local sceneN = tonumber(sceneNS) if sceneN > lastScene then -- find Highest numbered (last) slide lastScene = sceneN lastSceneName = sceneName end if (sceneN < firstScene) then -- find Lowest numbered (first) slide firstScene = sceneN firstSceneName = sceneName end if (sceneN > sceneNumber) and (sceneN < forwardScene) then -- find next sequential slide forwardScene = sceneN forwardSceneName = sceneName end if (sceneN < sceneNumber) and (sceneN > reverseScene) then -- find previous sequential slide reverseScene = sceneN reverseSceneName = sceneName end end end end if previousSlide then if reverseSceneName ~= nil then nextSceneName = reverseSceneName -- transition to next scene elseif useLooping then nextSceneName = lastSceneName -- transition to last scene end else if forwardSceneName ~= nil then nextSceneName = forwardSceneName -- transition to next scene elseif useLooping then nextSceneName = firstSceneName -- transition to first scene end end end end if nextSceneName ~= nil then for i, scn in ipairs(scenes) do -- find nextSceneName in scenes if nextSceneName == obs.obs_source_get_name(scn) then -- found it nextSceneNum = i break end end obs.obs_frontend_set_current_scene(scenes[nextSceneNum]) --transition to next scene end if previousSlide or nextSlide then -- reset hotkey flags nextSlide = false previousSlide = false checkScene() end obs.source_list_release(scenes) end ------------------------------------------------------------------------------------------------------------------ -- Checks current scene to see if it is part of a show... -- Prefix (1,2, or 3) matches current scene name, or scene is in the custom group list -- If a match is found the delay (if specified) is extracted and a delayed callback is -- initiated to advance_slidescene() ------------------------------------------------------------------------------------------------------------------ function checkScene() local scene = obs.obs_frontend_get_current_scene() local current_scene = obs.obs_source_get_name(scene) useCustomList = obs.obs_data_get_bool(script_sets, "useCustom") obs.obs_source_release(scene) usingCustomList = false if current_scene ~= nil then local found = false if useCustomList then -- see if scene is in the custom list if get_index_in_list(showSceneList, current_scene) ~= nil then -- Use Custom Scene List usingCustomList = true found = true end end if not usingCustomList then if current_scene:find(Prefix1 .. "%d+") == 1 then -- Use Prefix 1 prefixString = Prefix1 found = true elseif current_scene:find(Prefix2 .. "%d+") == 1 then -- Use Prefix 2 prefixString = Prefix2 found = true elseif current_scene:find(Prefix3 .. "%d+") == 1 then -- Use Prefix 3 prefixString = Prefix3 found = true end end if found then useAutoTiming = obs.obs_data_get_bool(script_sets, "useTimed") if previousSlide or nextSlide then obs.timer_remove(advance_slideScene) obs.timer_add(advance_slideScene, 50) elseif useAutoTiming then _, _, sceneDelay = current_scene:find(":(%d+)") if sceneDelay == nil then sceneDelay = "0" end if tonumber(sceneDelay) > 0 then obs.timer_add(advance_slideScene, sceneDelay * 1000) else obs.timer_add(advance_slideScene, slide_fade_speed * 1000) end end end end end --------------------------------------------------------------------------------------------------------------- -- OBS calls this whenever any event occurs. The Scene_changed event is used to check if the scene is part -- of one of the pre-defined slide shows by prefix or custom list --------------------------------------------------------------------------------------------------------------- function on_event(event) if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED then obs.timer_remove(advance_slideScene) checkScene() end if event == obs.OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED then local scenes = obs.obs_frontend_get_scenes() validScenesList = {} if scenes ~= nil then for _, scene in ipairs(scenes) do local name = obs.obs_source_get_name(scene) obs.obs_property_list_add_string(validListProp, name, name) validScenesList[#validScenesList + 1] = name end end obs.source_list_release(scenes) sceneChange = true end end ---------------------------------------------------------------------------------------------------------------- -- Updates the OBS editable list from the working custom scene list - Editable list array is created if empty ---------------------------------------------------------------------------------------------------------------- function updateListFromScenes() local editList = obs.obs_data_get_array(scripts_sets, "prep_list") if editList == nil then editList = obs.obs_data_array_create() -- create array if empty end if obs.obs_data_array_count(editList) > 0 then obs.obs_data_array_erase(editList, 0) -- erase existing editable list array end for i = 1, #showSceneList do local array_obj = obs.obs_data_create() obs.obs_data_set_string(array_obj, "value", showSceneList[i]) -- move scene name to list array object obs.obs_data_array_push_back(editList, array_obj) --add object to array obs.obs_data_release(array_obj) end obs.obs_data_set_array(script_sets, "prep_list", editList) -- save new list array obs.obs_data_array_release(editList) -- release editable list array obs.obs_properties_apply_settings(props, script_sets) -- update display end ------------------------------------------------------------------------------------------------------------------ -- Updates the working custom scene list from the OBS editable list -- Scenes must be in the list of existing scenes (validScenesList) and included only Once ------------------------------------------------------------------------------------------------------------------ function updateScenesFromList() local editList = obs.obs_data_get_array(script_sets, "prep_list") showSceneList = {} -- erase existing working list for i = 0, obs.obs_data_array_count(editList) - 1 do local item = obs.obs_data_array_item(editList, i) -- get editable list item local itemName = obs.obs_data_get_string(item, "value") -- get scene name from item if get_index_in_list(validScenesList, itemName) ~= nil then -- skip if nil if get_index_in_list(showSceneList, itemName) == nil then -- skip is already added showSceneList[#showSceneList + 1] = itemName -- add scene to working list end end obs.obs_data_release(item) -- release item end obs.obs_data_array_release(editList) -- release editable list array end ------------------------------------------------------------------------------------------------------------------ -- Helper function to find item in an array list and return index number or nil if not found ------------------------------------------------------------------------------------------------------------------ function get_index_in_list(list, q_item) for index, item in ipairs(list) do if item == q_item then return index end end return nil end function save_scene(props, p) local SelectScene = obs.obs_data_get_string(script_sets, "sceneOptions") if SelectScene ~= "" then if get_index_in_list(showSceneList, SelectScene) == nil then showSceneList[#showSceneList + 1] = SelectScene updateListFromScenes() end end return true end function clearList(props, p) showSceneList = {} updateListFromScenes() return true end function useCustomListOption(props, prop, settings) useCustomList = obs.obs_data_get_bool(settings, "useCustom") obs.obs_property_set_visible(obs.obs_properties_get(props, "sceneOptions"), useCustomList) obs.obs_property_set_visible(obs.obs_properties_get(props, "select_button"), useCustomList) obs.obs_property_set_visible(obs.obs_properties_get(props, "clearButton"), useCustomList) obs.obs_property_set_visible(obs.obs_properties_get(props, "prep_list"), useCustomList) checkScene() return true end function useAutoTimingOption(props, prop, settings) -- This next local 'value' will be WRONG the first time returned from obs_data_get_bool -- (bug?) Tracing in Visual Studio shows JSON File Read Error causes it to return false when true (maybe should return nil type?) -- in subsequent calls to useAutoTimingOption, obs_data_get_bool will return the correct 'true" setting local value = obs.obs_data_get_bool(settings, "useTimed") if useAutoReading then useAutoTiming = value end obs.obs_property_set_visible(obs.obs_properties_get(props, "SlideSpeed"), useAutoTiming) if useAuto then checkScene() -- start slideshow if on valid slide scene end useAutoReading = true -- use local value any time other than first time return true end ----------------------------------------------------------------------------------------------------------------------- -- Help Button Text ----------------------------------------------------------------------------------------------------------------------- help = "============ Usage Help (Click to Close) ==========\n\n " .. "* The slideshow functions when any\n " .. " valid slide scene is active.\n" .. "* Three unique slideshow 'Prefix' are supported.\n" .. "* Prefix groups have common prefix like 'Slide'.\n" .. " followed by a slide number. e.g. 'Slide1, Slide2, ...'\n " .. "* Numbered slides must be sequential but not consecutive.\n" .. "* Slideshow stops with any active scene\n" .. " not part of a slideshow group.\n" .. " * Adding ':sec' after the scene (slide) name, \n" .. " e.g. Slide3:15 overrides default slide time.\n" .. "* Slide timing does not include scene transition time. \n" .. "* Add Scene to Group button adds the Current Scenes.\n" .. " selection to the Custom Scenes List.\n" .. "* Custom slideshow scenes can be added,\n" .. " ordered, and deleted in the edit list.\n" .. "* Only existing scenes are allowed in the Custom Group.\n" .. "* Custom Group takes precedence over Prefix Groups.\n" ----------------------------------------------------------------------------------------------------------------------- -- Show/Hide the Help Text ----------------------------------------------------------------------------------------------------------------------- function show_help_button(props, prop, settings) local hb = obs.obs_properties_get(props, "show_help_button") showhelp = not showhelp if showhelp then obs.obs_property_set_description(hb, help) else obs.obs_property_set_description(hb, "Show Useage Help") end return true end ----------------------------------------------------------------------------------------------------------------------- -- Script Properties ----------------------------------------------------------------------------------------------------------------------- function script_properties() script_props = obs.obs_properties_create() obs.obs_properties_add_button(script_props, "show_help_button", "Show Useage Help", show_help_button) local P1prop = obs.obs_properties_add_text( script_props, "Prefix1", "Slide Prefix1: ", obs.OBS_TEXT_DEFAULT ) local P2prop = obs.obs_properties_add_text( script_props, "Prefix2", "Slide Prefix2: ", obs.OBS_TEXT_DEFAULT ) local P3 = obs.obs_properties_add_text( script_props, "Prefix3", "Slide Prefix3: ", obs.OBS_TEXT_DEFAULT ) local autoAdvance = obs.obs_properties_add_bool(script_props, "useTimed", "Use Timed Slides") local slideDelay = obs.obs_properties_add_int_slider( script_props, "SlideSpeed", "Slide Time (sec): ", 1, 30, 1 ) obs.obs_properties_add_bool(script_props, "loopSlides", "Loop Slides") local customList = obs.obs_properties_add_bool(script_props, "useCustom", "Use Custom Group") obs.obs_property_set_modified_callback(customList, useCustomListOption) local validListProp = obs.obs_properties_add_list( script_props, "sceneOptions", "Scene Select: ", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING ) local scenes = obs.obs_frontend_get_scenes() validScenesList = {} if scenes ~= nil then for _, scene in ipairs(scenes) do local name = obs.obs_source_get_name(scene) obs.obs_property_list_add_string(validListProp, name, name) validScenesList[#validScenesList + 1] = name end end obs.source_list_release(scenes) local selectSceneProp = obs.obs_properties_add_button(script_props, "select_button", "Add Above Scene to Custom Group", save_scene) local clearScenesProp = obs.obs_properties_add_button(script_props, "clearButton", "Clear Custom Group", clearList) local editScenesProp = obs.obs_properties_add_editable_list( script_props, "prep_list", "Custom Group: ", obs.OBS_EDITABLE_LIST_TYPE_STRINGS, nil, nil ) obs.obs_property_set_modified_callback(editScenesProp, edits_made) obs.obs_property_set_visible(validListProp, useCustomList) obs.obs_property_set_visible(selectSceneProp, useCustomList) obs.obs_property_set_visible(clearScenesProp, useCustomList) obs.obs_property_set_visible(editScenesProp, useCustomList) useAutoTiming = obs.obs_data_get_bool(script_sets, "useTimed") obs.obs_property_set_visible(slideDelay, useAutoTiming) obs.obs_property_set_modified_callback(autoAdvance, useAutoTimingOption) updateScenesFromList() checkScene() return script_props end ----------------------------------------------------------------------------------------------------------------------- -- Script Description ----------------------------------------------------------------------------------------------------------------------- function script_description() return [[
SceneShow Ver: 1.0 by Strato
A SlideShow from Scenes

]] end ----------------------------------------------------------------------------------------------------------------------- -- Update variables on change ----------------------------------------------------------------------------------------------------------------------- function script_update(settings) slide_fade_speed = obs.obs_data_get_int(settings, "SlideSpeed") Prefix1 = obs.obs_data_get_string(settings, "Prefix1") Prefix2 = obs.obs_data_get_string(settings, "Prefix2") Prefix3 = obs.obs_data_get_string(settings, "Prefix3") useLooping = obs.obs_data_get_bool(settings, "loopSlides") useAutoTiming = obs.obs_data_get_bool(settings, "useTimed") checkScene() end ----------------------------------------------------------------------------------------------------------------------- -- Callback if changes are made directly to Custom Group edit list ----------------------------------------------------------------------------------------------------------------------- function edits_made(props, prop, settings) updateScenesFromList(props, prop, settings) updateListFromScenes() return true end ----------------------------------------------------------------------------------------------------------------------- -- Default values for initial start of script ----------------------------------------------------------------------------------------------------------------------- function script_defaults(settings) obs.obs_data_set_default_bool(settings, "useCustom", false) obs.obs_data_set_default_bool(settings, "useAutoTiming", false) obs.obs_data_set_default_int(settings, "SlideSpeed", 3) end ----------------------------------------------------------------------------------------------------------------------- -- Script Load housekeeping. Prefix values, Frontend Event Callback, and Hotkeys ----------------------------------------------------------------------------------------------------------------------- function script_load(settings) script_sets = settings slide_fade_speed = obs.obs_data_get_int(settings, "SlideSpeed") Prefix1 = obs.obs_data_get_string(settings, "Prefix1") Prefix2 = obs.obs_data_get_string(settings, "Prefix2") Prefix3 = obs.obs_data_get_string(settings, "Prefix3") obs.obs_frontend_add_event_callback(on_event) Previous_hotkey_id = obs.obs_hotkey_register_frontend("scene_show_previous", "SceneShow Previous Slide Scene", previous_slide) local hotkey_save_array = obs.obs_data_get_array(settings, "scene_show_previous") obs.obs_hotkey_load(Previous_hotkey_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) Next_hotkey_id = obs.obs_hotkey_register_frontend("scene_show_next", "SceneShow Next Slide Scene", next_slide) local hotkey_save_array = obs.obs_data_get_array(settings, "scene_show_next") obs.obs_hotkey_load(Next_hotkey_id, hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) end ----------------------------------------------------------------------------------------------------------------------- -- Hotkey save ----------------------------------------------------------------------------------------------------------------------- function script_save(settings) local hotkey_save_array = obs.obs_hotkey_save(Previous_hotkey_id) obs.obs_data_set_array(settings, "scene_show_previous", hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) local hotkey_save_array = obs.obs_hotkey_save(Next_hotkey_id) obs.obs_data_set_array(settings, "scene_show_next", hotkey_save_array) obs.obs_data_array_release(hotkey_save_array) end ----------------------------------------------------------------------------------------------------------------------- -- The Previous Slide Hotkey function ----------------------------------------------------------------------------------------------------------------------- function previous_slide(pressed) if not pressed then return end obs.timer_remove(advance_slideScene) previousSlide = true checkScene() end ----------------------------------------------------------------------------------------------------------------------- -- The Next Slide Hotkey function ----------------------------------------------------------------------------------------------------------------------- function next_slide(pressed) if not pressed then return end obs.timer_remove(advance_slideScene) nextSlide = true checkScene() end