|
| 1 | +--[[ |
| 2 | +OBS RTC Timecode Generator for Text Sources |
| 3 | +Generate timecode (HH:MM:SS:FF) for a text source based on the computers RTC (Real-Time Clock) |
| 4 | +For updates, documentation and more information see the project page at: https://github.com/spessoni/obs-timecode-text |
| 5 | +---------------------------------- |
| 6 | +v1.0 (2021/06/08): Initial Release |
| 7 | +]] |
| 8 | + |
| 9 | +obs = obslua |
| 10 | + |
| 11 | +-- Globals |
| 12 | +source_active = false -- The Source is in Program |
| 13 | +cb_active = false -- Callback timer is active |
| 14 | +frame = 0 -- Frame counter |
| 15 | +last_time = "" -- Last time (Stored) |
| 16 | +frame_text = "" -- Frame text (String) |
| 17 | + |
| 18 | +-- Properties |
| 19 | +source_name = "" -- Text source name |
| 20 | +time_mode = "24 Hour" -- Clock mode |
| 21 | +show_frame = false -- Enable showing frames ":FF" |
| 22 | +pre_text = "" -- Text before timecode |
| 23 | +post_text = "" -- Text after timecode |
| 24 | +keep_updated = false -- Update when not in program |
| 25 | + |
| 26 | +-- Debug |
| 27 | +debug = false -- Enable or disable script output |
| 28 | + |
| 29 | +-- Local Settings |
| 30 | +Format12hr = "%I:%M:%S" |
| 31 | +Format24hr = "%H:%M:%S" |
| 32 | +FormatAmPm = "%p" |
| 33 | + |
| 34 | +-- Description |
| 35 | + -- Logo Image - Base64 Encoded Image - https://www.base64-image.de/ |
| 36 | +img_logo = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALwAAAA3BAMAAABEED2DAAAAGFBMVEUAAAAAAAD+//5XV1ebm5sqKysZGhrKysuA4j2YAAAAAXRSTlMAQObYZgAABChJREFUWMPtlk1z2jAQhvsTuvjjjiTqsyxoz5YVchbU5Gw+pndD/n/flQx1GSATmmbaTnfaQa8kPzGr1bt86IPeOHrse+J3TeOpQWBIlLUUI8fMCoueZ3dRcWBrS6v44IqVZ7W6ii+F+EQCgaGmhe7xCWYkWTEhi4Ulq+P8PAvDEjM5HobK1XW8c4UvnHO0EXKAd52raVrMyercLpWDCtsPTu0YnxSu00/KFY2kJ3kd7/HWikd2X+iIPy4QKZ7eiuWEYoQ/1jBt9ExPY3zfcibzTr+MT1VbTi7hhRjikfQs4CtKx9iT7eQa+iremEIXxvh0TEnEb4z5PMB/UeVSGnPC0xAPpfDyt45W8tHqH/iFEJMBHhQ+2qt4LN3Al9KTQrmlQER8hnIb4HOPt2+aq3hpqxv4pdBMIcZfzD2Gt3O/uJV7/0VGPJJzHX9/5SSF5rqP+INz+oSPdY/hWd0XUK+pe4GIeAyqE/7arYVS12/tP+SYb4n/Hy8H/dZ4F3zJNolbEp2YVQWl4ZeqXUQbxWK/VUbbl7DfEQZjqDnm58RqEm4X4wb46WPhdGIPbSppOynZDcrDo0yEsw/whk4LOEVLiE0BZaE2AY+dFVThE7xIiqY5D95QuEc5TE5yNL7Cl5NSQ+CJLlX0TcLZykpQH4tnKseWn1At8Gwa1sPHvoVGYgl7yc4UDOMiXhxExCsik04oYfxogK8o0QEvqhMeVglLkxHvaQE8XcZLISflrPGMpwEePabH7xpvoZCNnRo9NCu87FLM2MsG+ObrZfzYLvlox+d4nFWPX4TDrBJpaxxtPOg9d5UBHtsfLuPNZlLWRp/jjfE9foPcG6MTuS3UaG9mZK3UGWx/gC+6fXsZT8kp93Q596N4tDK3fe6XouVSHCZnLekFfJvbVEb8eoCf07rH06jHezsfJId7osqEPsdn5b6N+Bpf3tZTlQhT7he16TSSY8LX3SrTVRYq1v2ed/q0aE74L9IUM4WPc/zPt3bLd7EUhf751uZWyPbs1vrczk54UGR2Xvf5KvzOptzj365B4UG1lDUr/D/9KufYQfEGfmKVYeB3mGxZ4ZNOv8Pj9r/cMf/H+7TGj38Efuppq2t8ssCQUu2g2rjo5pQff9umDt1q6iBfgS81nCvewRVR9Bw4kGeV8PVd80Vmdey14j58JoGXWVeJ563wQe25Geq8Cw3n2K0Q9+DzGkAxFZVQpfBBwVUrG1JV/zKeY2GFrYQtOh/UnoEx8/fi2TEFIuKNQu63n5gSDZHW7KYcx2ZYvQp/cLZCd+si/nONbpVUwMfK6TQqR7UBf3A1Wef0/cmJzXCQg3JMlLP65dxncoiH6pthyy1V/Q68bOzYPizFLfz9yck7oSgVYn9/5Sw9bbQhMkFtPCVBtXHRfKbcGB1EookX/ihLe0P8d01J5Amxe24GAAAAAElFTkSuQmCC" |
| 37 | +version = "Version 1.00 - <a href='https://github.com/spessoni/obs-timecode-text'>spessoni</a>" |
| 38 | +description = [[ |
| 39 | +<center><img width='188' height='55' src=']] .. img_logo .. [['/><br>]] .. version .. [[</center> |
| 40 | +<h3>Generates Real-Time Clock (RTC) Timecode on a text source</h3> |
| 41 | +<p><strong>Tips:</strong></p> |
| 42 | +<ul> |
| 43 | +<li>Synchronize your computer's clock with an internet time server.</li> |
| 44 | +<li>A monospaced / fixed-width font is recommended to prevent resizing of the timecode every frame.</li> |
| 45 | +<li>Enable "Update when not in program" only if needed, otherwise keep it disabled to reduce CPU load.</li> |
| 46 | +<li>Enabling "Show Frames" will require more processing power because every frame must be rendered.</li> |
| 47 | +</ul> |
| 48 | +]] |
| 49 | + |
| 50 | +-------------------------------------------------------------------------------- |
| 51 | + |
| 52 | +-- A function named script_description returns the description shown to |
| 53 | +-- the user |
| 54 | +function script_description() |
| 55 | + return description |
| 56 | +end |
| 57 | + |
| 58 | +-- Function to set the text |
| 59 | +function set_text() |
| 60 | + -- Get HH:MM:SS in requested time format |
| 61 | + local format = Format24hr |
| 62 | + if time_mode == "12 Hour + AM/PM" or time_mode == "12 Hour" then |
| 63 | + format = Format12hr |
| 64 | + end |
| 65 | + local time = os.date(format) |
| 66 | + |
| 67 | + -- Get AM/PM if requested |
| 68 | + local ampm = "" |
| 69 | + if time_mode == "12 Hour + AM/PM" then |
| 70 | + ampm = " " .. os.date(FormatAmPm) |
| 71 | + end |
| 72 | + |
| 73 | + -- Update frame counter if enabled |
| 74 | + frame_text = "" |
| 75 | + if show_frame then |
| 76 | + -- Check if "HH:MM:SS" has changed, if it has, reset the frames |
| 77 | + if time ~= last_time then |
| 78 | + frame = 0 |
| 79 | + end |
| 80 | + |
| 81 | + -- Create ":FF" text to add to end of "HH:MM:SS" |
| 82 | + frame_text = ":" .. string.format("%02d", frame) |
| 83 | + |
| 84 | + -- Store last "HH:MM:SS" value to check on next run |
| 85 | + last_time = time |
| 86 | + |
| 87 | + -- Increment frame counter for next run |
| 88 | + frame = frame + 1 |
| 89 | + end |
| 90 | + |
| 91 | + -- Create the text string |
| 92 | + local text = pre_text .. time .. frame_text .. ampm .. post_text |
| 93 | + |
| 94 | + -- If source exists then update the text |
| 95 | + local source = obs.obs_get_source_by_name(source_name) |
| 96 | + if source ~= nil then |
| 97 | + local settings = obs.obs_data_create() |
| 98 | + obs.obs_data_set_string(settings, "text", text) |
| 99 | + obs.obs_source_update(source, settings) |
| 100 | + obs.obs_data_release(settings) |
| 101 | + obs.obs_source_release(source) |
| 102 | + end |
| 103 | + |
| 104 | +end |
| 105 | + |
| 106 | +function timer_callback() |
| 107 | + if debug then print ("TIMER CALLBACK: Triggered") end |
| 108 | + -- Timer callback is only called if we are NOT using frames |
| 109 | + set_text() |
| 110 | +end |
| 111 | + |
| 112 | +function script_tick(seconds) |
| 113 | + -- Only update every frame if frames are required |
| 114 | + if (keep_updated or source_active) and show_frame then |
| 115 | + set_text() |
| 116 | + end |
| 117 | +end |
| 118 | + |
| 119 | +---------------------------------------------------------- |
| 120 | + |
| 121 | +-- A function named script_properties defines the properties that the user |
| 122 | +-- can change for the entire script module itself |
| 123 | +function script_properties() |
| 124 | + local props = obs.obs_properties_create() |
| 125 | + |
| 126 | + -- Text Source |
| 127 | + local p = obs.obs_properties_add_list(props, "source", "Text Source", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) |
| 128 | + local sources = obs.obs_enum_sources() |
| 129 | + if sources ~= nil then |
| 130 | + for _, source in ipairs(sources) do |
| 131 | + source_id = obs.obs_source_get_unversioned_id(source) |
| 132 | + if source_id == "text_gdiplus" or source_id == "text_ft2_source" then |
| 133 | + local name = obs.obs_source_get_name(source) |
| 134 | + obs.obs_property_list_add_string(p, name, name) |
| 135 | + end |
| 136 | + end |
| 137 | + end |
| 138 | + obs.source_list_release(sources) |
| 139 | + |
| 140 | + -- Timecode Format Mode |
| 141 | + local p_mode = obs.obs_properties_add_list(props, "time_mode", "Mode", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING) |
| 142 | + obs.obs_property_list_add_string(p_mode, "24 Hour", "24 Hour") |
| 143 | + obs.obs_property_list_add_string(p_mode, "12 Hour", "12 Hour") |
| 144 | + obs.obs_property_list_add_string(p_mode, "12 Hour + AM/PM", "12 Hour + AM/PM") |
| 145 | + |
| 146 | + -- Show Frames Checkbox |
| 147 | + local p_show_frame = obs.obs_properties_add_bool(props, "show_frame", "Show Frames") |
| 148 | + obs.obs_property_set_long_description(p_show_frame, "<b>NOTE:</b> This may require more CPU usage") |
| 149 | + |
| 150 | + -- Prefix Text |
| 151 | + obs.obs_properties_add_text(props, "pre_text", "Prefix Text", obs.OBS_TEXT_DEFAULT) |
| 152 | + |
| 153 | + -- Suffix Text |
| 154 | + obs.obs_properties_add_text(props, "post_text", "Suffix Text", obs.OBS_TEXT_DEFAULT) |
| 155 | + |
| 156 | + -- Update when not in Program Checkbox |
| 157 | + local p_keep_updated = obs.obs_properties_add_bool(props, "keep_updated", "Update when not in program") |
| 158 | + obs.obs_property_set_long_description(p_keep_updated, "Timecode will be updated even when not in program.\nThis is useful for projectors and isolated recording.") |
| 159 | + |
| 160 | + return props |
| 161 | +end |
| 162 | + |
| 163 | +-- A function named script_update will be called when settings are changed |
| 164 | +function script_update(settings) |
| 165 | + source_name = obs.obs_data_get_string(settings, "source") |
| 166 | + time_mode = obs.obs_data_get_string(settings, "time_mode") |
| 167 | + show_frame = obs.obs_data_get_bool(settings, "show_frame") |
| 168 | + pre_text = obs.obs_data_get_string(settings, "pre_text") |
| 169 | + post_text = obs.obs_data_get_string(settings, "post_text") |
| 170 | + keep_updated = obs.obs_data_get_bool(settings, "keep_updated") |
| 171 | + |
| 172 | + -- Check if source is active (in PGM), enable time callback if needed |
| 173 | + source_active = get_sceneitem_from_source_name_in_current_scene(source_name) |
| 174 | + |
| 175 | + -- Check what state we need to put the timer callback initially |
| 176 | + -- TODO: We could probably shorten this and just call activated(source_active). All this logic will happen in the function anyways. |
| 177 | + if (source_active or keep_updated) and not show_frame then |
| 178 | + if debug then print ("script_update(): Timer Callback ENABLED") end |
| 179 | + cb_toggle(true) |
| 180 | + else |
| 181 | + if debug then print ("script_update(): Timer Callback DISABLED") end |
| 182 | + cb_toggle(false) |
| 183 | + end |
| 184 | + |
| 185 | +end |
| 186 | + |
| 187 | +-- A function named script_defaults will be called to set the default settings |
| 188 | +function script_defaults(settings) |
| 189 | + obs.obs_data_set_default_string(settings, "source", "") |
| 190 | + obs.obs_data_set_default_string(settings, "time_mode", "24 Hour") |
| 191 | + obs.obs_data_set_default_bool(settings, "show_frame", false) |
| 192 | + obs.obs_data_set_default_string(settings, "pre_text", "") |
| 193 | + obs.obs_data_set_default_string(settings, "post_text", "") |
| 194 | + obs.obs_data_set_default_bool(settings, "keep_updated", false) |
| 195 | +end |
| 196 | + |
| 197 | +-- a function named script_load will be called on startup |
| 198 | +function script_load(settings) |
| 199 | + -- Connect hotkey and activation/deactivation signal callbacks |
| 200 | + -- |
| 201 | + -- NOTE: These particular script callbacks do not necessarily have to |
| 202 | + -- be disconnected, as callbacks will automatically destroy themselves |
| 203 | + -- if the script is unloaded. So there's no real need to manually |
| 204 | + -- disconnect callbacks that are intended to last until the script is |
| 205 | + -- unloaded. |
| 206 | + local sh = obs.obs_get_signal_handler() |
| 207 | + obs.signal_handler_connect(sh, "source_activate", source_activated) |
| 208 | + obs.signal_handler_connect(sh, "source_deactivate", source_deactivated) |
| 209 | +end |
| 210 | + |
| 211 | +-- Toggle the callback on or off |
| 212 | +-- active: true = activate callback timer false = stop callback timer |
| 213 | +function cb_toggle(active) |
| 214 | + if debug then print ("cb_toggle(" .. tostring(active) .. ") Current Callback Active = " .. tostring(cb_active) ) end |
| 215 | + |
| 216 | + -- Check if callback is already in the requested state |
| 217 | + if cb_active == active then |
| 218 | + if debug then print ("cb_toggle(IGNORE) Matches current state, ignoring...") end |
| 219 | + return |
| 220 | + end |
| 221 | + |
| 222 | + -- Activate / Deactivate timer callback |
| 223 | + if active then |
| 224 | + if debug then print ("TIMER CALLBACK: Enabled") end |
| 225 | + obs.timer_add(timer_callback, 1000) |
| 226 | + -- Immediately trigger an update, otherwise old time will be visible for 1000ms |
| 227 | + set_text() |
| 228 | + else |
| 229 | + if debug then print ("TIMER CALLBACK: Disabled") end |
| 230 | + obs.timer_remove(timer_callback) |
| 231 | + end |
| 232 | + |
| 233 | + -- Set callback status flag |
| 234 | + cb_active = active |
| 235 | + |
| 236 | +end |
| 237 | + |
| 238 | +-- Callback: ANY source is now in program |
| 239 | +function source_activated(cd) |
| 240 | + activate_signal(cd, true) |
| 241 | +end |
| 242 | + |
| 243 | +-- Callback: ANY source is nolonger in program |
| 244 | +function source_deactivated(cd) |
| 245 | + activate_signal(cd, false) |
| 246 | +end |
| 247 | + |
| 248 | +-- Called when ANY source is activated/deactivated |
| 249 | +function activate_signal(cd, activating) |
| 250 | + local source = obs.calldata_source(cd, "source") |
| 251 | + if source ~= nil then |
| 252 | + -- Check if source activate/deactivate is OUR source |
| 253 | + local name = obs.obs_source_get_name(source) |
| 254 | + if (name == source_name) then |
| 255 | + activated(activating) |
| 256 | + end |
| 257 | + end |
| 258 | +end |
| 259 | + |
| 260 | +-- Handle our source becoming active or inactive |
| 261 | +function activated(active) |
| 262 | + if debug then print ("activated(" .. tostring(active) .. ") Is source active: " .. tostring(source_active) ) end |
| 263 | + |
| 264 | + -- Set source status flag |
| 265 | + source_active = active |
| 266 | + |
| 267 | + -- Toggle Callback Timer ON if (source is active OR keep_update is check) AND are not showing frames |
| 268 | + -- TODO: We can probably just call cb_toggle without an if statement and just send the logic results to cb_toggle, but it reads easier |
| 269 | + if (active or keep_updated) and not show_frame then |
| 270 | + cb_toggle(true) |
| 271 | + else |
| 272 | + cb_toggle(false) |
| 273 | + end |
| 274 | + |
| 275 | +end |
| 276 | + |
| 277 | +-- Retrieves the scene item of the given source name in the current scene or nil if not found |
| 278 | +function get_sceneitem_from_source_name_in_current_scene(name) |
| 279 | + local result_sceneitem = nil |
| 280 | + local current_scene_as_source = obs.obs_frontend_get_current_scene() |
| 281 | + if current_scene_as_source then |
| 282 | + local current_scene = obs.obs_scene_from_source(current_scene_as_source) |
| 283 | + result_sceneitem = obs.obs_scene_find_source_recursive(current_scene, name) |
| 284 | + obs.obs_source_release(current_scene_as_source) |
| 285 | + end |
| 286 | + return result_sceneitem |
| 287 | +end |
0 commit comments