Skip to content

Commit b63b88a

Browse files
committed
Initial Commit
0 parents  commit b63b88a

7 files changed

Lines changed: 675 additions & 0 deletions

File tree

LICENSE

Lines changed: 339 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# OBS RTC Timecode Generator for Text Sources
2+
3+
Generate timecode (HH:MM:SS:FF) for a text source based on the computers RTC (Real-Time Clock)
4+
5+
![RTC TCG Script Logo](docs/logo-large.png?raw=true)
6+
7+
**Tips:**
8+
9+
* Synchronize your computer's clock with an internet time server.
10+
* Enable "Update when not in program" only if needed. Otherwise, keep it disabled to lower the CPU load
11+
* A [monospaced / fixed-width font](https://en.wikipedia.org/wiki/Monospaced_font) is recommended to prevent resizing of the timecode every frame
12+
* Remember that Enabling "Show Frames" will require more processing power because every frame must be rendered.
13+
14+
## Why is this useful?
15+
16+
![Timecode Demo](docs/tc-animation.gif?raw=true)
17+
18+
The script was written to create accurate timecode which is based on the computer's time of day. It can be used for creating slates, time labels on security footage, live cameras, etc. It is useful for checking latency with other live sources and encoders.
19+
20+
## Settings
21+
22+
![Settings Screenshot](docs/settings.png?raw=true)
23+
24+
* **Text Source**: This is the source to which the timecode text will be applied.
25+
* **Mode**: Select the clock mode of either 24 Hour, 12 Hour, or 12 Hour with AM/PM displayed.
26+
* **Show Frames**: Checking this will display the frames component (:FF) of the timecode. This could potentially slow down older computers because every frame must be rendered. You can keep an eye on this with the CPU usage box at the bottom right corner of the OBS application.
27+
* **Prefix Text**: This text will be applied *in front of the timecode*. You may want to add a space at the end, if needed, to add padding to the text.
28+
* **Suffix Text**: This text will be applied *at the end of the timecode*. You may want to add a space at the end, if needed, to add padding to the text.
29+
* **Update when not in program**: When checked, the time will continue to be updated even when not in program. This can be useful when making isolated recordings for later logging, keeping timecode running on the OBS multiviewer, sending via NDI feeds, ect. By default it is disabled to minimize CPU load, especially with "*show frames*" checked.
30+
31+
## Technical Background
32+
33+
[SMPTE timecode](https://en.wikipedia.org/wiki/SMPTE_timecode) is a standard to label video frames. It is used in television and film productions for synchronization and logging. This program will use your computer's [real-time clock (RTC)](https://en.wikipedia.org/wiki/Real-time_clock) to generate this timecode. Currently, only non-drop frame (NDF) timecode is implemented, even if using a drop frame format such as 29.97 fps. Please note that "drop frame" is a labeling method of skipping certain numbers to prevent clock drift at fractional frame rates (30fps vs 29.97fps), NOT dropping actual frames.
34+
35+
The Lua `os.date` function only reliably returns seconds. To get frame accuracy, the frame number is incremented every frame, and then reset at the clock's ***second*** change. This works well and labels every frame regardless of frame rate base.
36+
37+
## Installation / Getting Started
38+
39+
1. [Download](https://github.com/spessoni/obs-timecode-text/releases) the latest version of ***timecode-text.lua***
40+
3. Create a text source which will display the timecode.
41+
2. Add script via *Tools > Scripts* menu.
42+
3. Configure the script settings.
43+
4. Make sure the source is in program or enable "Update when not in program"
44+
45+
## Troubleshooting
46+
47+
* **Timecode does not advance:** Make sure the source is in program and currently active or enable "Update when not in program". Also make sure the correct source is set and hasn't been renamed.
48+
* **Clock is not correct:** Make sure your computer's clock is displaying correctly, including time zone and daylight savings / standard time. You may also want to synchronize your computer's clock with an internet time server.
49+
* **My CPU usage goes up when I select frames:** Unfortunately this is unavoidable. Each frame is required to be processed and rendered. On most machines, this will only add 1-2% max to the CPU processing load.

docs/logo-large.png

1.91 KB
Loading

docs/logo-small.png

1.14 KB
Loading

docs/settings.png

2.43 KB
Loading

docs/tc-animation.gif

22.5 KB
Loading

timecode-text.lua

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
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

Comments
 (0)