Circle Timer

Nederig

New Member
Who knows how to implement a circle timer like this? At the end of which the scene will change
When I search I find only ordinary, unremarkable timers
 

Attachments

  • 26ddbf96d2a207c5e3aec56970d707399b7b9bae.png
    26ddbf96d2a207c5e3aec56970d707399b7b9bae.png
    17.4 KB · Views: 48

Suslik V

Active Member
Here is concept. As is. It is free.
The round timer for OBS Studio consist of customizable background, foreground, level bar(progress bar) textures, 60 segments of circle animation textures (for green screen effect) and round mask image for the level bar.
The round timer for obs is runs by script that modifies text source (the "time" itself) and fires hotkey 60 times (once for each 6 degree segment) + 1 final hotkey at the end. On each hotkey event next segment of the circle animation is shown (Image Slide Show source).

Usage of the example:
  1. Download the "TimerRoundExample.7z" archive (attached to this post), ~331KiB to download, CRC of the archive MD5:06355398495FDDAD5AF208DA172928B9 and unpack its contents to the "C:\Temp\TimerRoundExample" folder (make the one if it doesn't exist). If all done OK you will find: "C:\Temp\TimerRoundExample\Timer_Round_Example.json" and other files)
  2. Import Scene Collection file named "Timer_Round_Example.json"
  3. Switch to just imported Scene Collection named "Timer Round Example"
  4. When the scene "Scene 1 - from" selected, the round timer should start automatically (script simulates hotkey events that switches slides of the slide show source).
  5. When the round timer count ends OBS will switch to "Scene 2 - to" via the hotkey (script simulates additional hotkey event).

To set new time:
  1. Hide the Scene source named "Timer (round)" by clicking eye icon next to it.
  2. Open main menu Tools > Scripts
  3. Select the "countdown_for_round_clocktimer.lua" script from the list and modify the "Duration (minutes)" parameter in the script properties.
  4. Show the Scene source named "Timer (round)" by clicking eye icon next to it.

Now look how it was build and you can create similar round timer (circle, rectangle, any shape) in OBS with your own textures.

Some screenshots of the setup:
General view.
obs screen tmer round example01.png


Settings details.
obs screen tmer round example02.pngobs screen tmer round example03.pngobs screen tmer round example04.pngobs screen tmer round example05.pngobs screen tmer round example06.pngobs screen tmer round example07.pngobs screen tmer round example08.pngobs screen tmer round example09.png

Code:
16:42:44.900: - scene 'Scene 1 - from':
16:42:44.900:     - source: 'Text (GDI+)' (text_gdiplus_v2)
16:42:44.900:     - source: 'Timer (round)' (scene)
16:42:44.900: - scene 'Scene 2 - to':
16:42:44.901:     - source: 'Text (GDI+) 2' (text_gdiplus_v2)
16:42:44.901: - scene 'Timer (round)':
16:42:44.901:     - source: 'obs round timer back.png' (image_source)
16:42:44.901:     - source: 'Group' (group)
16:42:44.901:         - filter: 'Color Key' (color_key_filter_v2)
16:42:44.901:         - source: 'obs round timer level bar.png' (image_source)
16:42:44.901:             - filter: 'Image Mask/Blend (obs round timer level bar mask.png)' (mask_filter_v2)
16:42:44.901:         - source: 'Slides for timer progress' (slideshow)
16:42:44.901:     - source: 'obs round timer front.png' (image_source)
16:42:44.901:     - source: 'Timer digits' (text_gdiplus_v2)
Lua:
obs           = obslua
source_name   = ""
total_seconds = 0

cur_seconds   = 0
last_text     = ""
stop_text     = ""
activated     = false

hotkey_id     = obs.OBS_INVALID_HOTKEY_ID

-- Two digits display mode 00:00 vs 00:00:00
no_hours_mode = true

-- Number of slides -1 (number of segments of the disk, in range 1..60 because this timer counts only in seconds and min timer in seconds = 60)
segments_number = 60

segment_time_seconds = 1

-- Full list of the available hotkeys (OBS v29.1.2)
-- https://github.com/obsproject/obs-studio/blob/cb391a595d45aea0d710680c143eb90efe22998b/libobs/obs-hotkeys.h

-- Next segment hotkey
next_segment_key = "OBS_KEY_NUM9"
next_segment_key_modifires = {control=true} ---- {shift=true, alt=true, control=true, command=true}

-- Last segment hotkey
end_segment_key = "OBS_KEY_NUM8"
end_segment_key_modifires = {control=true}

----------------------------------------------------------

-- This function triggers OBS hotkey event
function send_hotkey(hotkey_id_name, key_modifiers)
    shift_mod = key_modifiers.shift or false
    control_mod = key_modifiers.control or false
    alt_mod = key_modifiers.alt or false
    command_mod = key_modifiers.command or false
    modifiers = 0

    if shift_mod then
        modifiers = bit.bor(modifiers, obs.INTERACT_SHIFT_KEY )
    end
 
    if control_mod then
        modifiers = bit.bor(modifiers, obs.INTERACT_CONTROL_KEY )
    end
 
    if alt_mod then
        modifiers = bit.bor(modifiers, obs.INTERACT_ALT_KEY )
    end
 
    if command_mod then
        modifiers = bit.bor(modifiers, obs.INTERACT_COMMAND_KEY )
    end

    combo = obs.obs_key_combination()
    combo.modifiers = modifiers
    combo.key = obs.obs_key_from_name(hotkey_id_name)

    obs.obs_hotkey_inject_event(combo, false)
    obs.obs_hotkey_inject_event(combo, true)
    obs.obs_hotkey_inject_event(combo, false)
end

-- Function to set the time text
function set_time_text()
    local seconds       = math.floor(cur_seconds % 60)
    local total_minutes = math.floor(cur_seconds / 60)
    local minutes
    local hours
    local text
    if no_hours_mode then
        minutes = math.floor(total_minutes)
        text    = string.format("%02d:%02d", minutes, seconds)
    else
        minutes = math.floor(total_minutes % 60)
        hours   = math.floor(total_minutes / 60)
        text    = string.format("%02d:%02d:%02d", hours, minutes, seconds)
    end

    if cur_seconds < 1 then
        text = stop_text
    end

    if text ~= last_text then
        local source = obs.obs_get_source_by_name(source_name)
        if source ~= nil then
            local settings = obs.obs_data_create()
            obs.obs_data_set_string(settings, "text", text)
            obs.obs_source_update(source, settings)
            obs.obs_data_release(settings)
            obs.obs_source_release(source)
        end

        -- Fire hotkey for the next segment
        if (cur_seconds % segment_time_seconds) == 0 then
            send_hotkey(next_segment_key, next_segment_key_modifires)
        end
    end

    -- Fire hotkey after the text source last changed
    if cur_seconds < 1 then
        send_hotkey(end_segment_key, end_segment_key_modifires)
    end

    last_text = text
end

function timer_callback()
    cur_seconds = cur_seconds - 1
    if cur_seconds < 0 then
        obs.remove_current_callback()
        cur_seconds = 0
    else
        set_time_text() -- Call only when cur_seconds >= 0
    end
end

function activate(activating)
    if activated == activating then
        return
    end

    activated = activating

    if activating then
        cur_seconds = total_seconds
        set_time_text()
        obs.timer_add(timer_callback, 1000)
    else
        obs.timer_remove(timer_callback)
    end
end

-- Called when a source is activated/deactivated
function activate_signal(cd, activating)
    local source = obs.calldata_source(cd, "source")
    if source ~= nil then
        local name = obs.obs_source_get_name(source)
        if (name == source_name) then
            activate(activating)
        end
    end
end

function source_activated(cd)
    activate_signal(cd, true)
end

function source_deactivated(cd)
    activate_signal(cd, false)
end

function reset(pressed)
    if not pressed then
        return
    end

    activate(false)
    local source = obs.obs_get_source_by_name(source_name)
    if source ~= nil then
        local active = obs.obs_source_active(source)
        obs.obs_source_release(source)
        activate(active)
    end
end

function reset_button_clicked(props, p)
    reset(true)
    return false
end

----------------------------------------------------------

-- A function named script_properties defines the properties that the user
-- can change for the entire script module itself
function script_properties()
    local props = obs.obs_properties_create()
    obs.obs_properties_add_int(props, "duration", "Duration (minutes)", 1, 100000, 1)

    local p = obs.obs_properties_add_list(props, "source", "Text Source", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING)
    local sources = obs.obs_enum_sources()
    if sources ~= nil then
        for _, source in ipairs(sources) do
            source_id = obs.obs_source_get_unversioned_id(source)
            if source_id == "text_gdiplus" or source_id == "text_ft2_source" then
                local name = obs.obs_source_get_name(source)
                obs.obs_property_list_add_string(p, name, name)
            end
        end
    end
    obs.source_list_release(sources)

    obs.obs_properties_add_text(props, "stop_text", "Final Text", obs.OBS_TEXT_DEFAULT)
    obs.obs_properties_add_button(props, "reset_button", "Reset Timer", reset_button_clicked)

    return props
end

-- A function named script_description returns the description shown to
-- the user
function script_description()
    local ctrl_next = next_segment_key_modifires.control or false
    local alt_next = next_segment_key_modifires.alt or false
    local shift_next = next_segment_key_modifires.shift or false
    local command_next = next_segment_key_modifires.command or false
    local ctrl_end = end_segment_key_modifires.control or false
    local alt_end = end_segment_key_modifires.alt or false
    local shift_end = end_segment_key_modifires.shift or false
    local command_end = end_segment_key_modifires.command or false
    local modifiers_next = ""
    local modifiers_end = ""

    if ctrl_next then
        modifiers_next = modifiers_next .. "CTRL + "
    end

    if alt_next then
        modifiers_next = modifiers_next .. "ALT + "
    end

    if shift_next then
        modifiers_next = modifiers_next .. "SHIFT + "
    end

    if command_next then
        modifiers_next = modifiers_next .. "COMMAND + "
    end

    if ctrl_end then
        modifiers_end = modifiers_end .. "CTRL + "
    end

    if alt_end then
        modifiers_end = modifiers_end .. "ALT + "
    end

    if shift_end then
        modifiers_end = modifiers_end .. "SHIFT + "
    end

    if command_end then
        modifiers_end = modifiers_end .. "COMMAND + "
    end

    return "Sets a text source to act as a countdown timer when the source is active.\n\nFires hotkey " .. modifiers_next .. next_segment_key ..
    " at 1/60 intervals.\nEnds with " .. modifiers_end .. end_segment_key .. ".\n\nMade by Jim, modified by Suslik V"
end

-- A function named script_update will be called when settings are changed
function script_update(settings)
    activate(false)

    total_seconds = obs.obs_data_get_int(settings, "duration") * 60
    source_name = obs.obs_data_get_string(settings, "source")
    stop_text = obs.obs_data_get_string(settings, "stop_text")

    segment_time_seconds = math.floor(total_seconds / segments_number)

    reset(true)
end

-- A function named script_defaults will be called to set the default settings
function script_defaults(settings)
    obs.obs_data_set_default_int(settings, "duration", 5)
    obs.obs_data_set_default_string(settings, "stop_text", "00:00")
end

-- A function named script_save will be called when the script is saved
--
-- NOTE: This function is usually used for saving extra data (such as in this
-- case, a hotkey's save data).  Settings set via the properties are saved
-- automatically.
function script_save(settings)
    local hotkey_save_array = obs.obs_hotkey_save(hotkey_id)
    obs.obs_data_set_array(settings, "reset_hotkey", hotkey_save_array)
    obs.obs_data_array_release(hotkey_save_array)
end

-- a function named script_load will be called on startup
function script_load(settings)
    -- Connect hotkey and activation/deactivation signal callbacks
    --
    -- NOTE: These particular script callbacks do not necessarily have to
    -- be disconnected, as callbacks will automatically destroy themselves
    -- if the script is unloaded.  So there's no real need to manually
    -- disconnect callbacks that are intended to last until the script is
    -- unloaded.
    local sh = obs.obs_get_signal_handler()
    obs.signal_handler_connect(sh, "source_activate", source_activated)
    obs.signal_handler_connect(sh, "source_deactivate", source_deactivated)

    hotkey_id = obs.obs_hotkey_register_frontend("reset_timer_thingy", "Reset Timer", reset)
    local hotkey_save_array = obs.obs_data_get_array(settings, "reset_hotkey")
    obs.obs_hotkey_load(hotkey_id, hotkey_save_array)
    obs.obs_data_array_release(hotkey_save_array)
end

Notes.
  • The Color Key filter for the progress bar (applied to the whole Group) may require additional adjustments.
  • The Image Mask/Blend filter's Path may not be caught by the Check the Missing Files check of OBS. So, if you are using different directory for unpacking the round timer example files - make sure the all paths are correct.
  • Using Color Key filter is not optimal. Maybe someone can write code for the Image Mask/Blend filter that will enable use of any OBS source (not only a file) as the source of the mask.

TimerRoundExample.7z ~331KiB to download, CRC of the archive MD5:06355398495FDDAD5AF208DA172928B9
 

Attachments

  • TimerRoundExample.7z
    331.8 KB · Views: 156
Top