Skip to content

Instantly share code, notes, and snippets.

@horseslmao
Created January 10, 2026 09:59
Show Gist options
  • Select an option

  • Save horseslmao/82153d07b33a10092b713fbb26ac4dc6 to your computer and use it in GitHub Desktop.

Select an option

Save horseslmao/82153d07b33a10092b713fbb26ac4dc6 to your computer and use it in GitHub Desktop.
Quick Modification of MMLTech's Stream health HUD. Adds FPS and padding to make it amenable to scrolling. Original Work: https://obsproject.com/forum/resources/stream-health-hud.2286/
-- Stream Health HUD (Overlay Text Source Updater)
-- Targets Text (GDI+) sources (id = text_gdiplus_v3)
-- Updates a Text source with:
-- • Stream/Record time
-- • Dropped frames
-- • CPU %
-- • FPS
-- • Bitrate (kbps, if available)
local obs = obslua
----------------------------------------------------------
-- Settings / state
----------------------------------------------------------
local hud_enabled = false
local hud_source_name = ""
local update_interval_ms = 1000
local fps_num = 0
local fps_den = 0
local cpu_info = nil
local prev_bytes = nil
local prev_time = nil
----------------------------------------------------------
-- Helpers
----------------------------------------------------------
local function log(msg)
print("[StreamHealthHUD] " .. msg)
end
local function update_video_info()
local vi = obs.obs_video_info()
if obs.obs_get_video_info(vi) then
fps_num = vi.fps_num
fps_den = vi.fps_den
log(string.format("Video FPS: %d/%d", fps_num, fps_den))
else
fps_num = 30
fps_den = 1
log("Failed to get video info, defaulting to 30 FPS")
end
end
local function frames_to_seconds(frames)
if fps_num == 0 or fps_den == 0 then return 0 end
return math.floor(frames * fps_den / fps_num)
end
local function format_hms(sec)
sec = math.floor(sec or 0)
if sec < 0 then sec = 0 end
local h = math.floor(sec / 3600)
local m = math.floor((sec % 3600) / 60)
local s = sec % 60
return string.format("%02d:%02d:%02d", h, m, s)
end
local function get_cpu_usage_string()
if cpu_info ~= nil and obs.os_cpu_usage_info_query ~= nil then
local v = obs.os_cpu_usage_info_query(cpu_info)
if v ~= nil then
return string.format("%.1f%%", v)
end
end
return "n/a"
end
local function get_active_output()
local out = obs.obs_frontend_get_streaming_output()
if out ~= nil then
if obs.obs_output_active(out) then
return out
end
obs.obs_output_release(out)
end
out = obs.obs_frontend_get_recording_output()
if out ~= nil then
if obs.obs_output_active(out) then
return out
end
obs.obs_output_release(out)
end
return nil
end
local function build_hud_text()
local out = get_active_output()
local time_str = "Offline."
local dropped = 0
local bitrate_str = "n/a"
local ourfps = 0
if out ~= nil then
if obs.obs_output_get_total_frames ~= nil then
local frames = obs.obs_output_get_total_frames(out) or 0
local seconds = frames_to_seconds(frames)
time_str = format_hms(seconds)
else
time_str = "n/a"
end
if obs.obs_output_get_frames_dropped ~= nil then
dropped = obs.obs_output_get_frames_dropped(out) or 0
else
dropped = 0
end
if obs.obs_get_active_fps() ~= nil then
ourfps = obs.obs_get_active_fps() or 0
else
ourfps = 0
end
if obs.obs_output_get_total_bytes ~= nil then
local total_bytes = obs.obs_output_get_total_bytes(out) or 0
local now = os.time()
if prev_bytes ~= nil and prev_time ~= nil and now > prev_time then
local delta_bytes = total_bytes - prev_bytes
if delta_bytes < 0 then delta_bytes = 0 end
local delta_t = now - prev_time
local kbps = (delta_bytes * 8) / delta_t / 1000.0
bitrate_str = string.format("%.0f kbps", kbps)
else
bitrate_str = "…"
end
prev_bytes = total_bytes
prev_time = now
else
bitrate_str = "n/a"
end
obs.obs_output_release(out)
else
prev_bytes = nil
prev_time = nil
end
local cpu_str = get_cpu_usage_string()
return string.format(
"Time: %s | Dropped: %d | CPU: %s | FPS: %02d | Bitrate: %s ",
time_str,
dropped,
cpu_str,
ourfps,
bitrate_str
)
end
local function clear_hud_text()
if hud_source_name == nil or hud_source_name == "" then
return
end
local src = obs.obs_get_source_by_name(hud_source_name)
if src == nil then
return
end
local settings = obs.obs_source_get_settings(src)
if settings ~= nil then
obs.obs_data_set_string(settings, "text", "")
obs.obs_source_update(src, settings)
obs.obs_data_release(settings)
end
obs.obs_source_release(src)
end
local function update_hud()
if not hud_enabled then
return
end
if hud_source_name == nil or hud_source_name == "" then
return
end
local src = obs.obs_get_source_by_name(hud_source_name)
if src == nil then
log("update_hud: source not found: " .. tostring(hud_source_name))
return
end
local text = build_hud_text()
local settings = obs.obs_source_get_settings(src)
if settings ~= nil then
obs.obs_data_set_string(settings, "text", text)
obs.obs_source_update(src, settings)
obs.obs_data_release(settings)
end
obs.obs_source_release(src)
end
----------------------------------------------------------
-- Script API
----------------------------------------------------------
function script_description()
return [[Stream Health HUD (Text GDI+ v3)
Updates a Text (GDI+) source with:
• Stream/Record time
• Dropped frames
• CPU %
• Bitrate (kbps, if available)
• FPS
Make sure "Read from file" is DISABLED on the Text (GDI+) source.]]
end
function script_properties()
local props = obs.obs_properties_create()
obs.obs_properties_add_bool(
props,
"hud_enabled",
"Enable HUD"
)
local list = obs.obs_properties_add_list(
props,
"hud_source_name",
"HUD Text Source (Text GDI+)",
obs.OBS_COMBO_TYPE_LIST,
obs.OBS_COMBO_FORMAT_STRING
)
local sources = obs.obs_enum_sources()
if sources ~= nil then
for _, src in ipairs(sources) do
local id = obs.obs_source_get_id(src)
local name = obs.obs_source_get_name(src)
if id == "text_gdiplus_v3"
or id == "text_gdiplus_v2"
or id == "text_gdiplus_v1"
or id == "text_ft2_source"
or id == "text_ft2_source_v2" then
obs.obs_property_list_add_string(list, name, name)
log("Found Text source: " .. name .. " (id=" .. id .. ")")
else
log(string.format("Source found: '%s' (id=%s)", name, id))
end
end
obs.source_list_release(sources)
end
obs.obs_properties_add_int(
props,
"update_interval_ms",
"Update interval (ms)",
200, 5000, 100
)
return props
end
function script_defaults(settings)
obs.obs_data_set_default_bool(settings, "hud_enabled", false)
obs.obs_data_set_default_int(settings, "update_interval_ms", 1000)
obs.obs_data_set_default_string(settings, "hud_source_name", "")
end
function script_update(settings)
hud_enabled = obs.obs_data_get_bool(settings, "hud_enabled")
hud_source_name = obs.obs_data_get_string(settings, "hud_source_name")
update_interval_ms = obs.obs_data_get_int(settings, "update_interval_ms")
if update_interval_ms < 200 then
update_interval_ms = 200
end
obs.timer_remove(update_hud)
if hud_enabled then
obs.timer_add(update_hud, update_interval_ms)
log("HUD enabled, bound to source: " .. (hud_source_name or "<none>")
.. " every " .. tostring(update_interval_ms) .. " ms")
else
log("HUD disabled, clearing text")
clear_hud_text()
end
end
function script_load(settings)
update_video_info()
if obs.os_cpu_usage_info_start ~= nil then
cpu_info = obs.os_cpu_usage_info_start()
end
hud_enabled = obs.obs_data_get_bool(settings, "hud_enabled")
update_interval_ms = obs.obs_data_get_int(settings, "update_interval_ms")
if update_interval_ms < 200 then
update_interval_ms = 200
end
if hud_enabled then
obs.timer_add(update_hud, update_interval_ms)
end
end
function script_unload()
obs.timer_remove(update_hud)
clear_hud_text()
if cpu_info ~= nil and obs.os_cpu_usage_info_destroy ~= nil then
obs.os_cpu_usage_info_destroy(cpu_info)
cpu_info = nil
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment