Help with modifying Instant Replay Lua Script to incorporate Source Record

KGeetings

New Member
Hello, I want to first state that I am still fairly new to programming in Lua, and am definitely looking for help and suggestions for my code. So I have taken the Instant Replay script made by Jim and Exeldro, and have attempted to modify it. What I want to be able to do is use the multiple source record plugin (which if you're unfamiliar with allows you to have replay buffers on individual sources instead of whatever is live in your program scene). I have two sources that I want to record with Source Record, where each of these gets saved to their own folder. Whenever I click a hotkey to save those replays, I also want to have my script go and look for the most recent file in each directory, and set each file to a specific source. I think I've got the basics figured out, but I'm running into a few issues:
  1. The first issue is that whenever I restart OBS, my directories (and media sources) don't seem to be saved.
  2. The second issue is that that my script seems to be running before the replays are saved, so I always get the second to latest file, instead of the latest one.
  3. The third issue, and possible the most important, is that my sources don't seem to actually be updating to the a new file, and I'm not entirely sure why.
Here's the code, any and all help is greatly appreciated! It should also maybe be noted that I am currently using OBS 28.0.1

Lua:
obs         = obslua
source_name0 = ""
source_name1 = ""
hotkey_id   = obs.OBS_INVALID_HOTKEY_ID
attempts    = 0
last_replay0 = ""
last_replay1 = ""
directory0 = ""
directory1 = ""

----------------------------------------------------------
--modify lua script to get latest replay from a directory and set that as source0
--also grab the latest replay from a different directory and set that as source1

local function get_last_file_name(directory)
    print("directory: " .. directory)
    local files = {}
    local p = io.popen('dir "'..directory..'" /b')
    for file in p:lines() do
        table.insert(files, file)
    end
    table.sort(files)
    local dir_file_name = directory.."\\"..files[#files]
    return dir_file_name
end

function try_play()
    print("called try_play")
    --add a wait time here to allow the replay to finish saving
    --os.execute("timeout /t 5")
    local path0 = get_last_file_name(directory0)
    local path1 = get_last_file_name(directory1)
    print("path0 in try_play: "..path0)
    print("path1 in try_play: "..path1)

    -- If the path0 is valid and the source exists, update it with the replay file to play back the replay.
    -- Otherwise, stop attempting to replay after 10 retries
    last_replay0 = path0
    local source0 = obs.obs_get_source_by_name(source_name0)
    print("source0 in try_play: "..source_name0)
    if source0 ~= nil then
        local settings = obs.obs_data_create()
        source_id = obs.obs_source_get_id(source0)
        if source_id == "ffmpeg_source" then
            obs.obs_data_set_string(settings, "local_file", path0)
            obs.obs_data_set_bool(settings, "is_local_file", true)

            -- updating will automatically cause the source to
            -- refresh if the source is currently active
            obs.obs_source_update(source0, settings)
            print("updated source0")
        elseif source_id == "vlc_source" then
            -- "playlist"
            array = obs.obs_data_array_create()
            item = obs.obs_data_create()
            obs.obs_data_set_string(item, "value", path0)
            obs.obs_data_array_push_back(array, item)
            obs.obs_data_set_array(settings, "playlist", array)

            -- updating will automatically cause the source to
            -- refresh if the source is currently active
            obs.obs_source_update(source0, settings)
            obs.obs_data_release(item)
            obs.obs_data_array_release(array)
        end

        obs.obs_data_release(settings)
        obs.obs_source_release(source0)
    end

    last_replay1 = path1
    local source1 = obs.obs_get_source_by_name(source_name1)
    print("source1 in try_play: "..source_name1)
    if source1 ~= nil then
        local settings = obs.obs_data_create()
        source_id = obs.obs_source_get_id(source1)
        if source_id == "ffmpeg_source" then
            obs.obs_data_set_string(settings, "local_file", path1)
            obs.obs_data_set_bool(settings, "is_local_file", true)

            -- updating will automatically cause the source to
            -- refresh if the source is currently active
            obs.obs_source_update(source1, settings)
            print("updated source1")
        elseif source_id == "vlc_source" then
            -- "playlist"
            array = obs.obs_data_array_create()
            item = obs.obs_data_create()
            obs.obs_data_set_string(item, "value", path1)
            obs.obs_data_array_push_back(array, item)
            obs.obs_data_set_array(settings, "playlist", array)

            -- updating will automatically cause the source to
            -- refresh if the source is currently active
            obs.obs_source_update(source1, settings)
            obs.obs_data_release(item)
            obs.obs_data_array_release(array)
        end

        obs.obs_data_release(settings)
        obs.obs_source_release(source1)
    end
    --obs.remove_current_callback()
end

-- The "Instant Replay" hotkey callback
function instant_replay(pressed)
    if not pressed then
        return
    end

    print("called try_play from hotkey")
    try_play()
end

----------------------------------------------------------

-- A function named script_update will be called when settings are changed
function script_update(settings)
    source_name0 = obs.obs_data_get_string(settings, "source0")
    source_name1 = obs.obs_data_get_string(settings, "source1")
    directory0 = obs.obs_data_get_string(settings, "directory0")
    directory1 = obs.obs_data_get_string(settings, "directory1")
end

-- A function named script_description returns the description shown to the user
function script_description()
    return "When the \"Instant Replay\" hotkey is triggered, grab the latest replay buffers from the directories and set them to the corresponding media sources.\n\nMade by KGeetings, modified from the work of Jim and Exeldro"
end

-- A function named script_properties defines the properties that the user
-- can change for the entire script module itself
function script_properties()
    props = obs.obs_properties_create()

    local p = obs.obs_properties_add_list(props, "source0", "Media Source 1", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING)
    local p1 = obs.obs_properties_add_list(props, "source1", "Media Source 2", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING)
    
    -- let the user define the directories to search for the last file
    directory0 = obs.obs_properties_add_path(props, "directory0", "Directory 1", obs.OBS_PATH_DIRECTORY, nil, nil)
    directory1 = obs.obs_properties_add_path(props, "directory1", "Directory 2", obs.OBS_PATH_DIRECTORY, nil, nil)

    local sources = obs.obs_enum_sources()
    if sources ~= nil then
        for _, source in ipairs(sources) do
            source_id = obs.obs_source_get_id(source)
            if source_id == "ffmpeg_source" then
                local name = obs.obs_source_get_name(source)
                obs.obs_property_list_add_string(p, name, name)
                obs.obs_property_list_add_string(p1, name, name)
            elseif source_id == "vlc_source" then
                local name = obs.obs_source_get_name(source)
                obs.obs_property_list_add_string(p, name, name)
                obs.obs_property_list_add_string(p1, name, name)
            else
                -- obs.script_log(obs.LOG_INFO, source_id)
            end
        end
    end
    obs.source_list_release(sources)

    return props
end


-- A function named script_load will be called on startup
function script_load(settings)
    hotkey_id = obs.obs_hotkey_register_frontend("instant_replay.trigger", "Instant Replay Duo", instant_replay)
    local hotkey_save_array = obs.obs_data_get_array(settings, "instant_replay.trigger")
    obs.obs_hotkey_load(hotkey_id, hotkey_save_array)
    obs.obs_data_array_release(hotkey_save_array)
end

-- A function named script_save will be called when the script is saved
--
-- NOTE: This function is usually used for saving extra data (such as in this
-- case, a hotkey's save data). Settings set via the properties are saved automatically.
function script_save(settings)
    local hotkey_save_array = obs.obs_hotkey_save(hotkey_id)
    obs.obs_data_set_array(settings, "instant_replay.trigger", hotkey_save_array)
    obs.obs_data_array_release(hotkey_save_array)
end
 

KGeetings

New Member
So I believe I've got a solution to issue #2. I have also re-worked how I grab the latest file from the directory. In this code, I've switched to just storing a test value for each directory instead of putting it in the settings (but I would love to find a way to have the user specify their own location!) I think my switch to a different method of finding the latest file also fixed my issue of the sources not updating, so potentially issue #3 is solved as well.

Lua:
obs         = obslua
source_name0 = ""
source_name1 = ""
hotkey_id   = obs.OBS_INVALID_HOTKEY_ID
attempts    = 0
last_replay0 = ""
last_replay1 = ""
directory0 = "C:/Users/Geetings/Videos/test"
directory1 = "C:/Users/Geetings/Videos/test2"

----------------------------------------------------------
--modify lua script to get latest replay from a directory and set that as source0
--also grab the latest replay from a different directory and set that as source1

local function get_last_file_name(directory)
    local command = 'dir /a-d /o-d /tc /b "'..directory..'" 2>nul:'
    -- /tw for last modified file
    -- /tc for last created file
    local pipe = io.popen(command)
    local file_name = pipe:read()
    pipe:close()
    local dir_file_name = directory.."\\"..file_name
    return dir_file_name
end

function sleep(s)
    local ntime = os.time() + s
    repeat until os.time() > ntime
end

function try_play()
    print("called try_play")
    -- Wait 3 seconds before trying to find the latest file
    -- this is to give the replay buffer time to finish writing the file
    sleep(3)
    local path0 = get_last_file_name(directory0)
    local path1 = get_last_file_name(directory1)
    print("path0 in try_play: "..path0)
    print("path1 in try_play: "..path1)

    -- If the path0 is valid and the source exists, update it with the replay file to play back the replay.
    -- Otherwise, stop attempting to replay after 10 retries
    last_replay0 = path0
    local source0 = obs.obs_get_source_by_name(source_name0)
    print("source0 in try_play: "..source_name0)
    if source0 ~= nil then
        local settings = obs.obs_data_create()
        source_id = obs.obs_source_get_id(source0)
        if source_id == "ffmpeg_source" then
            obs.obs_data_set_string(settings, "local_file", path0)
            obs.obs_data_set_bool(settings, "is_local_file", true)

            -- updating will automatically cause the source to
            -- refresh if the source is currently active
            obs.obs_source_update(source0, settings)
            print("updated source0")
        elseif source_id == "vlc_source" then
            -- "playlist"
            array = obs.obs_data_array_create()
            item = obs.obs_data_create()
            obs.obs_data_set_string(item, "value", path0)
            obs.obs_data_array_push_back(array, item)
            obs.obs_data_set_array(settings, "playlist", array)

            -- updating will automatically cause the source to
            -- refresh if the source is currently active
            obs.obs_source_update(source0, settings)
            obs.obs_data_release(item)
            obs.obs_data_array_release(array)
        end

        obs.obs_data_release(settings)
        obs.obs_source_release(source0)
    end

    last_replay1 = path1
    local source1 = obs.obs_get_source_by_name(source_name1)
    print("source1 in try_play: "..source_name1)
    if source1 ~= nil then
        local settings = obs.obs_data_create()
        source_id = obs.obs_source_get_id(source1)
        if source_id == "ffmpeg_source" then
            obs.obs_data_set_string(settings, "local_file", path1)
            obs.obs_data_set_bool(settings, "is_local_file", true)

            -- updating will automatically cause the source to
            -- refresh if the source is currently active
            obs.obs_source_update(source1, settings)
            print("updated source1")
        elseif source_id == "vlc_source" then
            -- "playlist"
            array = obs.obs_data_array_create()
            item = obs.obs_data_create()
            obs.obs_data_set_string(item, "value", path1)
            obs.obs_data_array_push_back(array, item)
            obs.obs_data_set_array(settings, "playlist", array)

            -- updating will automatically cause the source to
            -- refresh if the source is currently active
            obs.obs_source_update(source1, settings)
            obs.obs_data_release(item)
            obs.obs_data_array_release(array)
        end

        obs.obs_data_release(settings)
        obs.obs_source_release(source1)
    end
    --obs.remove_current_callback()
end

-- The "Instant Replay" hotkey callback
function instant_replay(pressed)
    if not pressed then
        return
    end

    print("called try_play from hotkey")
    print("last_replay0: "..last_replay0)
    print("last_replay1: "..last_replay1)
    print("source_name0: "..source_name0)
    print("source_name1: "..source_name1)
    print("directory0: "..directory0)
    print("directory1: "..directory1)
    try_play()
end

----------------------------------------------------------

-- A function named script_update will be called when settings are changed
function script_update(settings)
    source_name0 = obs.obs_data_get_string(settings, "source0")
    source_name1 = obs.obs_data_get_string(settings, "source1")
    --directory0 = obs.obs_data_get_string(settings, "directory0")
    --directory1 = obs.obs_data_get_string(settings, "directory1")
end

-- A function named script_description returns the description shown to the user
function script_description()
    return "When the \"Instant Replay\" hotkey is triggered, grab the latest replay buffers from the directories and set them to the corresponding media sources.\n\nMade by KGeetings, modified from the work of Jim and Exeldro"
end

-- A function named script_properties defines the properties that the user
-- can change for the entire script module itself
function script_properties()
    props = obs.obs_properties_create()

    local p = obs.obs_properties_add_list(props, "source0", "Media Source 1", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING)
    local p1 = obs.obs_properties_add_list(props, "source1", "Media Source 2", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING)
   
    -- let the user define the directories to search for the last file
    --directory0 = obs.obs_properties_add_path(props, "directory0", "Directory 1", obs.OBS_PATH_DIRECTORY, nil, nil)
    --directory1 = obs.obs_properties_add_path(props, "directory1", "Directory 2", obs.OBS_PATH_DIRECTORY, nil, nil)

    local sources = obs.obs_enum_sources()
    if sources ~= nil then
        for _, source in ipairs(sources) do
            source_id = obs.obs_source_get_id(source)
            if source_id == "ffmpeg_source" then
                local name = obs.obs_source_get_name(source)
                obs.obs_property_list_add_string(p, name, name)
                obs.obs_property_list_add_string(p1, name, name)
            elseif source_id == "vlc_source" then
                local name = obs.obs_source_get_name(source)
                obs.obs_property_list_add_string(p, name, name)
                obs.obs_property_list_add_string(p1, name, name)
            else
                -- obs.script_log(obs.LOG_INFO, source_id)
            end
        end
    end
    obs.source_list_release(sources)

    return props
end


-- A function named script_load will be called on startup
function script_load(settings)
    source_name0 = obs.obs_data_get_string(settings, "source0")
    source_name1 = obs.obs_data_get_string(settings, "source1")
    --directory0 = obs.obs_data_get_string(settings, "directory0")
    --directory1 = obs.obs_data_get_string(settings, "directory1")

    hotkey_id = obs.obs_hotkey_register_frontend("instant_replay.trigger", "Instant Replay Duo", instant_replay)
    local hotkey_save_array = obs.obs_data_get_array(settings, "instant_replay.trigger")
    obs.obs_hotkey_load(hotkey_id, hotkey_save_array)
    obs.obs_data_array_release(hotkey_save_array)
end

-- A function named script_save will be called when the script is saved
--
-- NOTE: This function is usually used for saving extra data (such as in this
-- case, a hotkey's save data). Settings set via the properties are saved automatically.
function script_save(settings)
    local hotkey_save_array = obs.obs_hotkey_save(hotkey_id)
    obs.obs_data_set_array(settings, "instant_replay.trigger", hotkey_save_array)
    obs.obs_data_array_release(hotkey_save_array)
end
 

KGeetings

New Member
What was the solution you found, I'm trying to do something similar with live sports just on a Mac
So I ended up not using this, as I was unfortunately not able to get Exeldro's OBS Source Record to actually record good quality videos with the laptop that I had, as I kept getting errors in OBS. I should also note that what I did get working isn't very elegant, and requires a manual sleep time before checking, which can be changed on line 36 "sleep(3)" to be longer if necessary.

If you do end up finding ways to improve this code or some other way to record two streams at once, I would love to know!

Lua:
--[[ Modified by KGeetings ]]

obs         = obslua
source_name0 = ""
source_name1 = ""
hotkey_id   = obs.OBS_INVALID_HOTKEY_ID
attempts    = 0
last_replay0 = ""
last_replay1 = ""
directory_name0 = ""
directory_name1 = ""

----------------------------------------------------------
--modify lua script to get latest replay from a directory and set that as source0
--also grab the latest replay from a different directory and set that as source1

local function get_last_file_name(directory)
    local command = 'dir /a-d /o-d /tc /b "'..directory..'" 2>nul:'
    -- /tw for last modified file
    -- /tc for last created file
    local pipe = io.popen(command)
    local file_name = pipe:read()
    pipe:close()
    local dir_file_name = directory.."\\"..file_name
    return dir_file_name
end

function sleep(s)
    local ntime = os.time() + s
    repeat until os.time() > ntime
end

function try_play()
    -- Wait 3 seconds before trying to find the latest file
    -- this is to give the replay buffer time to finish writing the file
    sleep(3)
    local directory0 = directory_name0
    local directory1 = directory_name1
    local path0 = get_last_file_name(directory0)
    local path1 = get_last_file_name(directory1)

    -- If the path0 is valid and the source exists, update it with the replay file to play back the replay.
    -- Otherwise, stop attempting to replay after 10 retries
    last_replay0 = path0
    local source0 = obs.obs_get_source_by_name(source_name0)
    if source0 ~= nil then
        local settings = obs.obs_data_create()
        source_id = obs.obs_source_get_id(source0)
        if source_id == "ffmpeg_source" then
            obs.obs_data_set_string(settings, "local_file", path0)
            obs.obs_data_set_bool(settings, "is_local_file", true)

            -- updating will automatically cause the source to
            -- refresh if the source is currently active
            obs.obs_source_update(source0, settings)
        elseif source_id == "vlc_source" then
            -- "playlist"
            array = obs.obs_data_array_create()
            item = obs.obs_data_create()
            obs.obs_data_set_string(item, "value", path0)
            obs.obs_data_array_push_back(array, item)
            obs.obs_data_set_array(settings, "playlist", array)

            -- updating will automatically cause the source to
            -- refresh if the source is currently active
            obs.obs_source_update(source0, settings)
            obs.obs_data_release(item)
            obs.obs_data_array_release(array)
        end

        obs.obs_data_release(settings)
        obs.obs_source_release(source0)
    end

    last_replay1 = path1
    local source1 = obs.obs_get_source_by_name(source_name1)
    if source1 ~= nil then
        local settings = obs.obs_data_create()
        source_id = obs.obs_source_get_id(source1)
        if source_id == "ffmpeg_source" then
            obs.obs_data_set_string(settings, "local_file", path1)
            obs.obs_data_set_bool(settings, "is_local_file", true)

            -- updating will automatically cause the source to
            -- refresh if the source is currently active
            obs.obs_source_update(source1, settings)
        elseif source_id == "vlc_source" then
            -- "playlist"
            array = obs.obs_data_array_create()
            item = obs.obs_data_create()
            obs.obs_data_set_string(item, "value", path1)
            obs.obs_data_array_push_back(array, item)
            obs.obs_data_set_array(settings, "playlist", array)

            -- updating will automatically cause the source to
            -- refresh if the source is currently active
            obs.obs_source_update(source1, settings)
            obs.obs_data_release(item)
            obs.obs_data_array_release(array)
        end

        obs.obs_data_release(settings)
        obs.obs_source_release(source1)
    end
    --obs.remove_current_callback()
end

-- The "Instant Replay" hotkey callback
function instant_replay(pressed)
    if not pressed then
        return
    end

    try_play()
end

----------------------------------------------------------

-- A function named script_update will be called when settings are changed
function script_update(settings)
    source_name0 = obs.obs_data_get_string(settings, "source0")
    source_name1 = obs.obs_data_get_string(settings, "source1")
    directory_name0 = obs.obs_data_get_string(settings, "directory0")
    directory_name1 = obs.obs_data_get_string(settings, "directory1")
end

-- A function named script_description returns the description shown to the user
function script_description()
    return "When the \"Instant Replay\" hotkey is triggered, grab the latest replay buffers from the directories and set them to the corresponding media sources.\n\nMade by KGeetings"
end

-- A function named script_properties defines the properties that the user
-- can change for the entire script module itself
function script_properties()
    props = obs.obs_properties_create()

    local p = obs.obs_properties_add_list(props, "source0", "Media Source 1", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING)
    local p1 = obs.obs_properties_add_list(props, "source1", "Media Source 2", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING)
    
    -- let the user define the directories to search for the last file
    local dir0 = obs.obs_properties_add_path(props, "directory0", "Directory 1", obs.OBS_PATH_DIRECTORY, NULL, NULL)
    local dir1 = obs.obs_properties_add_path(props, "directory1", "Directory 2", obs.OBS_PATH_DIRECTORY, NULL, NULL)

    local sources = obs.obs_enum_sources()
    if sources ~= nil then
        for _, source in ipairs(sources) do
            source_id = obs.obs_source_get_id(source)
            if source_id == "ffmpeg_source" then
                local name = obs.obs_source_get_name(source)
                obs.obs_property_list_add_string(p, name, name)
                obs.obs_property_list_add_string(p1, name, name)
            elseif source_id == "vlc_source" then
                local name = obs.obs_source_get_name(source)
                obs.obs_property_list_add_string(p, name, name)
                obs.obs_property_list_add_string(p1, name, name)
            else
                -- obs.script_log(obs.LOG_INFO, source_id)
            end
        end
    end
    obs.source_list_release(sources)

    return props
end

-- A function named script_load will be called on startup
function script_load(settings)
    source_name0 = obs.obs_data_get_string(settings, "source0")
    source_name1 = obs.obs_data_get_string(settings, "source1")
    directory_name0 = obs.obs_data_get_string(settings, "directory0")
    directory_name1 = obs.obs_data_get_string(settings, "directory1")

    hotkey_id = obs.obs_hotkey_register_frontend("instant_replay.trigger", "Instant Replay Duo", instant_replay)
    local hotkey_save_array = obs.obs_data_get_array(settings, "instant_replay.trigger")
    obs.obs_hotkey_load(hotkey_id, hotkey_save_array)
    obs.obs_data_array_release(hotkey_save_array)
end

-- A function named script_save will be called when the script is saved
-- NOTE: This function is usually used for saving extra data (such as in this
-- case, a hotkey's save data). Settings set via the properties are saved automatically.
function script_save(settings)
    local hotkey_save_array = obs.obs_hotkey_save(hotkey_id)
    obs.obs_data_set_array(settings, "instant_replay.trigger", hotkey_save_array)
    obs.obs_data_array_release(hotkey_save_array)
end
 

KGeetings

New Member
I did just take another look, and I also found this plugin, which admittedly hasn't been updated in a little while, but appears to work? This might be a better solution, so let me know which one you go with and if it works out for you.
 
Top