Created
January 10, 2026 09:59
-
-
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/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- 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