Timer for record time

Tavalero

New Member
Hi, if I only have one monitor and want to record a "Let's Play," I’d like to see a timer on my screen showing how long the recording has been running—but I don't want the timer to appear in the final recording. Is something like this possible with OBS? If so, how?
 
Wrote a small Lua script for this — no plugin build needed.

Setup: Tools -> Scripts -> + -> pick the .lua file -> choose one of your Text (GDI+) sources in the dropdown.

Behaviour:
- counts from the moment recording starts
- freezes while recording is paused, and resumes without losing the elapsed time
- keeps the final time visible after you stop
- optional prefix (e.g. "REC ") and optional idle text
- reset button in the properties panel, plus a bindable hotkey

Implementation note: it polls obs_frontend_recording_active() and obs_frontend_recording_paused() on a 500ms timer rather than hooking frontend events, so it behaves the same across OBS versions. Elapsed time is accumulated in segments — each pause closes the current segment, each resume opens a new one, which is why pausing doesn't lose time. The text source is only updated when the rendered string actually changes, so an idle timer doesn't cause needless redraws.

What I verified, so you know how much to trust it:
- Lua syntax checked against OBS's own lua51.dll; the same harness passes all four scripts that ship with OBS
- every API key confirmed by enumerating the obslua module's Lua table rather than guessing
- the state machine exercised against a mock obslua with a fake clock — 12/12 assertions pass (start, pause freeze, resume accumulate, stop retain, idempotent updates, prefix, idle text)
- not yet run inside a live OBS session on my machine

So the logic is tested but the live integration isn't. If it misbehaves for you, tell me and I'll fix it — the OBS log would help a lot.

If you want mm:ss only, count-down instead of count-up, or different behaviour on stop, say so and I'll adjust it.

Here's the full script (save as obs-recording-timer.lua):

Code:
-- obs-recording-timer.lua
-- Recording Timer for OBS Studio
--
-- 作用:把一个文本源变成「录制时长计时器」。
--      开始录制时自动计时,暂停录制时冻结,停止后保留最终时长。
--
-- 安装:OBS → 工具 → 脚本 → 加号 → 选这个文件
-- 然后在下拉框里选一个「文本(GDI+)」源,即可生效。
--
-- 作者:<你的署名>
-- 许可:MIT

obs = obslua

-- ---------------------------------------------------------------------------
-- 设置项
-- ---------------------------------------------------------------------------
source_name     = ""
prefix          = ""
idle_text       = ""
reset_on_start  = true
hotkey_id = nil   -- 该常量枚举不到,不依赖;script_load 里必然先赋值

-- ---------------------------------------------------------------------------
-- 内部状态
-- ---------------------------------------------------------------------------
local state        = "idle"   -- idle | recording | paused
local elapsed_base = 0        -- 已完成分段的累计秒数
local seg_start    = 0        -- 当前分段的起点墙钟(os.time)
local last_text    = ""

-- ---------------------------------------------------------------------------
-- 工具函数
-- ---------------------------------------------------------------------------
local function format_hms(total)
	total = math.floor(total)
	if total < 0 then total = 0 end
	local s = total % 60
	local m = math.floor(total / 60) % 60
	local h = math.floor(total / 3600)
	return string.format("%02d:%02d:%02d", h, m, s)
end

local function total_seconds()
	if state == "recording" and seg_start > 0 then
		return elapsed_base + (os.time() - seg_start)
	end
	return elapsed_base
end

local function set_text(text)
	-- 只在文字真的变了的时候才去更新源(和官方 countdown.lua 同样的做法,
	-- 避免每秒都惊动一次 OBS 的无关重绘)
	if text == last_text then
		return
	end

	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

	last_text = text
end

-- ---------------------------------------------------------------------------
-- 计时循环
-- ---------------------------------------------------------------------------
local function tick()
	local active = obs.obs_frontend_recording_active()
	local paused = false
	if active then
		paused = obs.obs_frontend_recording_paused()
	end

	local new_state
	if not active then
		new_state = "idle"
	elseif paused then
		new_state = "paused"
	else
		new_state = "recording"
	end

	-- 状态迁移
	if new_state ~= state then
		if state == "idle" then
			-- 新一轮录制开始
			if reset_on_start then
				elapsed_base = 0
			end
			seg_start = os.time()
		elseif state == "recording" and new_state == "paused" then
			-- 暂停:把这一段封存起来
			if seg_start > 0 then
				elapsed_base = elapsed_base + (os.time() - seg_start)
			end
			seg_start = 0
		elseif state == "paused" and new_state == "recording" then
			-- 继续
			seg_start = os.time()
		elseif new_state == "idle" then
			-- 停止:封存最后一段,并保留最终值在画面上
			if state == "recording" and seg_start > 0 then
				elapsed_base = elapsed_base + (os.time() - seg_start)
			end
			seg_start = 0
		end

		state = new_state
	end

	-- 渲染
	if state == "idle" then
		if idle_text ~= "" then
			set_text(idle_text)
		end
	else
		set_text(prefix .. format_hms(total_seconds()))
	end
end

-- ---------------------------------------------------------------------------
-- 手动重置
-- ---------------------------------------------------------------------------
local function reset(pressed)
	if not pressed then
		return
	end
	elapsed_base = 0
	if state == "recording" then
		seg_start = os.time()
	end
	-- 强制重画
	local keep = last_text
	last_text = "\0"
	set_text(keep)
end

-- ---------------------------------------------------------------------------
-- 脚本接口
-- ---------------------------------------------------------------------------
function script_description()
	return "把文本源变成录制时长计时器。\n\n" ..
	       "开始录制自动计时,暂停时冻结,停止后保留最终时长。\n" ..
	       "适合放在画面角上,让观众/自己看到这段已经录了多久。"
end

function script_properties()
	local props = obs.obs_properties_create()

	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
			local 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, "prefix", "前缀(可选)", obs.OBS_TEXT_DEFAULT)
	obs.obs_properties_add_text(props, "idle_text", "未录制时显示(留空=不动)", obs.OBS_TEXT_DEFAULT)
	obs.obs_properties_add_bool(props, "reset_on_start", "每次开录清零")
	obs.obs_properties_add_button(props, "reset_button", "手动清零", reset_button_clicked)

	return props
end

function script_defaults(settings)
	obs.obs_data_set_default_string(settings, "prefix", "")
	obs.obs_data_set_default_string(settings, "idle_text", "")
	obs.obs_data_set_default_bool(settings, "reset_on_start", true)
end

function script_update(settings)
	source_name    = obs.obs_data_get_string(settings, "source")
	prefix         = obs.obs_data_get_string(settings, "prefix")
	idle_text      = obs.obs_data_get_string(settings, "idle_text")
	reset_on_start = obs.obs_data_get_bool(settings, "reset_on_start")
	last_text      = "\0"   -- 强制重画一次
end

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

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

function script_load(settings)
	hotkey_id = obs.obs_hotkey_register_frontend("recording_timer_reset", "录制计时器:清零", 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)

	-- 打印一行到 OBS 日志:脚本到底加载成功没有,用这个确认,别靠猜
	print("[obs-recording-timer] loaded ok")

	-- 每 500ms 轮询一次:不用事件回调,避免依赖不同版本的 frontend event 接口
	obs.timer_add(tick, 500)
end

function script_unload()
	obs.timer_remove(tick)
end
 
Back
Top