obs = obslua ---------------------------------------------------------- -- Globals ---------------------------------------------------------- source_name = "" copy_folder = "" use_copied_file = true interval = 500 -- ms between checks max_attempts = 20 -- how many times to check hotkey_save_id = obs.OBS_INVALID_HOTKEY_ID hotkey_prev_id = obs.OBS_INVALID_HOTKEY_ID hotkey_next_id = obs.OBS_INVALID_HOTKEY_ID hotkey_latest_id = obs.OBS_INVALID_HOTKEY_ID clear_hotkey_id = obs.OBS_INVALID_HOTKEY_ID attempts = 0 last_replay = "" replay_list = {} -- history of replay file paths current_index = 0 -- 1-based index into replay_list ---------------------------------------------------------- -- Helpers ---------------------------------------------------------- local function get_filename_from_path(path) if not path or path == "" then return nil end -- Works on Windows and Unix paths return path:match("([^\\/]+)$") end local function get_timestamp() local d = os.date("*t") return string.format( "%04d%02d%02d_%02d%02d%02d", d.year, d.month, d.day, d.hour, d.min, d.sec ) end local function ensure_folder_trailing_sep(path) if not path or path == "" then return "" end local last = string.sub(path, -1) if last == "/" or last == "\\" then return path end local sep = package.config:sub(1, 1) or "\\" return path .. sep end local function copy_file(src, dst) local in_f = io.open(src, "rb") if not in_f then obs.script_log(obs.LOG_ERROR, "Could not open source file: " .. tostring(src)) return false end local out_f = io.open(dst, "wb") if not out_f then obs.script_log(obs.LOG_ERROR, "Could not open destination file: " .. tostring(dst)) in_f:close() return false end local content = in_f:read("*all") out_f:write(content) in_f:close() out_f:close() return true end -- Always set VLC playlist to a single file (no mountains) local function update_vlc_source(path) if not path or path == "" then obs.script_log(obs.LOG_WARNING, "No path passed to update_vlc_source") return end local source = obs.obs_get_source_by_name(source_name) if source == nil then obs.script_log(obs.LOG_WARNING, "VLC source not found: " .. source_name) return end local source_id = obs.obs_source_get_id(source) if source_id ~= "vlc_source" then obs.script_log(obs.LOG_WARNING, "Source is not VLC source: " .. source_id) obs.obs_source_release(source) return end local settings = obs.obs_data_create() local playlist_array = obs.obs_data_array_create() local item = obs.obs_data_create() obs.obs_data_set_string(item, "value", path) obs.obs_data_array_push_back(playlist_array, item) obs.obs_data_set_array(settings, "playlist", playlist_array) obs.obs_source_update(source, settings) obs.obs_data_release(item) obs.obs_data_array_release(playlist_array) obs.obs_data_release(settings) obs.obs_source_release(source) obs.script_log(obs.LOG_INFO, "Set VLC playlist to single replay: " .. path) end local function push_replay(path) if not path or path == "" then return end table.insert(replay_list, path) current_index = #replay_list obs.script_log( obs.LOG_INFO, string.format("Replay added to history (%d total).", #replay_list) ) end ---------------------------------------------------------- -- Timer: wait for replay file, copy, update VLC ---------------------------------------------------------- function try_handle_replay() local replay_buffer = obs.obs_frontend_get_replay_buffer_output() if replay_buffer == nil then obs.script_log(obs.LOG_WARNING, "No active replay buffer while waiting for replay file.") obs.remove_current_callback() return end local cd = obs.calldata_create() local ph = obs.obs_output_get_proc_handler(replay_buffer) obs.proc_handler_call(ph, "get_last_replay", cd) local path = obs.calldata_string(cd, "path") obs.calldata_destroy(cd) obs.obs_output_release(replay_buffer) if path == nil or path == "" or path == last_replay then attempts = attempts + 1 if attempts >= max_attempts then obs.script_log(obs.LOG_WARNING, "Replay file did not appear in time.") obs.remove_current_callback() end return end last_replay = path local playback_path = path if copy_folder ~= nil and copy_folder ~= "" then local filename = get_filename_from_path(path) if not filename then obs.script_log(obs.LOG_ERROR, "Failed to extract filename from: " .. tostring(path)) else local base, ext = filename:match("^(.-)%.([^%.]+)$") if not base or not ext then base = filename ext = "" else ext = "." .. ext end local dst_dir = ensure_folder_trailing_sep(copy_folder) local dst_name = string.format("%s_%s%s", base, get_timestamp(), ext) local dst_path = dst_dir .. dst_name local ok = copy_file(path, dst_path) if ok then obs.script_log(obs.LOG_INFO, "Copied replay to: " .. dst_path) if use_copied_file then playback_path = dst_path end else obs.script_log(obs.LOG_ERROR, "Failed to copy replay to: " .. dst_path) end end end push_replay(playback_path) update_vlc_source(playback_path) obs.remove_current_callback() end ---------------------------------------------------------- -- Hotkeys ---------------------------------------------------------- -- F8: save replay (you bind this in OBS) function instant_replay(pressed) if not pressed then return end local replay_buffer = obs.obs_frontend_get_replay_buffer_output() if replay_buffer == nil then obs.script_log(obs.LOG_WARNING, "Instant Replay hotkey pressed, but no replay buffer output found.") return end local ph = obs.obs_output_get_proc_handler(replay_buffer) obs.proc_handler_call(ph, "save", nil) if obs.obs_output_active(replay_buffer) then attempts = 0 obs.timer_add(try_handle_replay, interval) else obs.script_log(obs.LOG_WARNING, "Tried to save replay, but replay buffer is not active.") end obs.obs_output_release(replay_buffer) end -- F9: previous replay function replay_prev(pressed) if not pressed then return end if #replay_list == 0 then obs.script_log(obs.LOG_WARNING, "No replays in history for Previous Replay.") return end if current_index > 1 then current_index = current_index - 1 end local path = replay_list[current_index] obs.script_log( obs.LOG_INFO, string.format("Previous Replay: index %d of %d", current_index, #replay_list) ) update_vlc_source(path) end -- F10: next replay function replay_next(pressed) if not pressed then return end if #replay_list == 0 then obs.script_log(obs.LOG_WARNING, "No replays in history for Next Replay.") return end if current_index < #replay_list then current_index = current_index + 1 end local path = replay_list[current_index] obs.script_log( obs.LOG_INFO, string.format("Next Replay: index %d of %d", current_index, #replay_list) ) update_vlc_source(path) end -- F11: jump to latest replay function replay_latest(pressed) if not pressed then return end if #replay_list == 0 then obs.script_log(obs.LOG_WARNING, "No replays in history for Latest Replay.") return end current_index = #replay_list local path = replay_list[current_index] obs.script_log( obs.LOG_INFO, string.format("Latest Replay: index %d of %d", current_index, #replay_list) ) update_vlc_source(path) end -- optional: clear playlist + history function clear_vlc_playlist(pressed) if not pressed then return end local source = obs.obs_get_source_by_name(source_name) if source ~= nil then local source_id = obs.obs_source_get_id(source) if source_id == "vlc_source" then local settings = obs.obs_data_create() local array = obs.obs_data_array_create() obs.obs_data_set_array(settings, "playlist", array) obs.obs_source_update(source, settings) obs.obs_data_release(settings) obs.obs_data_array_release(array) obs.script_log(obs.LOG_INFO, "Cleared VLC playlist for source: " .. source_name) else obs.script_log(obs.LOG_WARNING, "clear_vlc_playlist called, but source is not VLC.") end obs.obs_source_release(source) end replay_list = {} current_index = 0 obs.script_log(obs.LOG_INFO, "Cleared replay history.") end ---------------------------------------------------------- -- OBS Script API ---------------------------------------------------------- function script_description() return [[Instant Replay VLC (with history) Hotkeys (set these yourself in OBS): - Save Replay: saves replay buffer, waits for the file, (optionally) copies it, and sets VLC to that replay. Also adds it to history. - Previous Replay: step backward through history, update VLC to that file. - Next Replay: step forward through history, update VLC to that file. - Latest Replay: jump straight to the newest replay. - Clear playlist: clears VLC playlist AND history. Use the VLC source in an "Instant Replay" scene. Switch scenes only when the replay you want is selected.]] end function script_properties() local props = obs.obs_properties_create() -- VLC source selector local p = obs.obs_properties_add_list( props, "source", "VLC Video 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_id(source) if source_id == "vlc_source" then local name = obs.obs_source_get_name(source) obs.obs_property_list_add_string(p, name, name) end end obs.source_list_release(sources) end obs.obs_properties_add_path( props, "copy_folder", "Replay Copy Folder (optional)", obs.OBS_PATH_DIRECTORY, "", "" ) obs.obs_properties_add_bool( props, "use_copied_file", "Use copied file instead of original" ) obs.obs_properties_add_int(props, "interval", "Interval ms", 50, 10000, 50) obs.obs_properties_add_int(props, "max_attempts", "Max attempts", 1, 200, 1) return props end function script_defaults(settings) obs.obs_data_set_default_int(settings, "interval", 500) obs.obs_data_set_default_int(settings, "max_attempts", 20) obs.obs_data_set_default_bool(settings, "use_copied_file", true) end function script_update(settings) source_name = obs.obs_data_get_string(settings, "source") copy_folder = obs.obs_data_get_string(settings, "copy_folder") use_copied_file = obs.obs_data_get_bool(settings, "use_copied_file") interval = obs.obs_data_get_int(settings, "interval") max_attempts = obs.obs_data_get_int(settings, "max_attempts") end function script_load(settings) -- Save Replay hotkey (you bind this to F8) hotkey_save_id = obs.obs_hotkey_register_frontend( "instant_replay_vlc.trigger", "Instant Replay VLC: Save Replay", instant_replay ) local ha = obs.obs_data_get_array(settings, "instant_replay_vlc.trigger") obs.obs_hotkey_load(hotkey_save_id, ha) obs.obs_data_array_release(ha) -- Previous Replay hotkey (F9) hotkey_prev_id = obs.obs_hotkey_register_frontend( "instant_replay_vlc.prev", "Instant Replay VLC: Previous Replay", replay_prev ) ha = obs.obs_data_get_array(settings, "instant_replay_vlc.prev") obs.obs_hotkey_load(hotkey_prev_id, ha) obs.obs_data_array_release(ha) -- Next Replay hotkey (F10) hotkey_next_id = obs.obs_hotkey_register_frontend( "instant_replay_vlc.next", "Instant Replay VLC: Next Replay", replay_next ) ha = obs.obs_data_get_array(settings, "instant_replay_vlc.next") obs.obs_hotkey_load(hotkey_next_id, ha) obs.obs_data_array_release(ha) -- Latest Replay hotkey (F11) hotkey_latest_id = obs.obs_hotkey_register_frontend( "instant_replay_vlc.latest", "Instant Replay VLC: Latest Replay", replay_latest ) ha = obs.obs_data_get_array(settings, "instant_replay_vlc.latest") obs.obs_hotkey_load(hotkey_latest_id, ha) obs.obs_data_array_release(ha) -- Clear playlist + history hotkey (optional) clear_hotkey_id = obs.obs_hotkey_register_frontend( "instant_replay_vlc.clear_playlist", "Instant Replay VLC: Clear playlist + history", clear_vlc_playlist ) ha = obs.obs_data_get_array(settings, "instant_replay_vlc.clear_playlist") obs.obs_hotkey_load(clear_hotkey_id, ha) obs.obs_data_array_release(ha) end function script_save(settings) local ha = obs.obs_hotkey_save(hotkey_save_id) obs.obs_data_set_array(settings, "instant_replay_vlc.trigger", ha) obs.obs_data_array_release(ha) ha = obs.obs_hotkey_save(hotkey_prev_id) obs.obs_data_set_array(settings, "instant_replay_vlc.prev", ha) obs.obs_data_array_release(ha) ha = obs.obs_hotkey_save(hotkey_next_id) obs.obs_data_set_array(settings, "instant_replay_vlc.next", ha) obs.obs_data_array_release(ha) ha = obs.obs_hotkey_save(hotkey_latest_id) obs.obs_data_set_array(settings, "instant_replay_vlc.latest", ha) obs.obs_data_array_release(ha) ha = obs.obs_hotkey_save(clear_hotkey_id) obs.obs_data_set_array(settings, "instant_replay_vlc.clear_playlist", ha) obs.obs_data_array_release(ha) end