obs = obslua ffi = require("ffi") ------------------------------------------------------------ -- PlayFolder v1.3 -- Windows only ------------------------------------------------------------ ------------------------------------------------------------ -- Windows API ------------------------------------------------------------ ffi.cdef[[ typedef void* HANDLE; typedef const char* LPCSTR; typedef unsigned long DWORD; typedef struct { DWORD dwFileAttributes; DWORD ftCreationTime_dwLowDateTime; DWORD ftCreationTime_dwHighDateTime; DWORD ftLastAccessTime_dwLowDateTime; DWORD ftLastAccessTime_dwHighDateTime; DWORD ftLastWriteTime_dwLowDateTime; DWORD ftLastWriteTime_dwHighDateTime; DWORD nFileSizeHigh; DWORD nFileSizeLow; DWORD dwReserved0; DWORD dwReserved1; char cFileName[260]; char cAlternateFileName[14]; } WIN32_FIND_DATAA; HANDLE FindFirstFileA(LPCSTR lpFileName, WIN32_FIND_DATAA *lpFindFileData); int FindNextFileA(HANDLE hFindFile, WIN32_FIND_DATAA *lpFindFileData); int FindClose(HANDLE hFindFile); ]] local INVALID_HANDLE_VALUE = ffi.cast("HANDLE", -1) ------------------------------------------------------------ -- Configuration and state ------------------------------------------------------------ local source_name = "" local folder_path = "" local playlist = {} local index = 1 local active = false local connected_source_name = "" ------------------------------------------------------------ -- Description ------------------------------------------------------------ function script_description() return [[ PlayFolder v1.3 Automatically plays all video files found in a selected folder and refreshes the playlist whenever new replay files are added. Designed for replay workflows where video files are continuously created and must be played sequentially through a standard OBS Media Source. MEDIA SOURCE SETTINGS - Loop = OFF - Restart playback when source becomes active = OFF - Close file when inactive = OFF Use ONLY Media Source (NOT VLC Video Source). Windows only. ]] end ------------------------------------------------------------ -- File list ------------------------------------------------------------ local function is_supported_video(name) local lower_name = string.lower(name) return lower_name:match("%.mp4$") ~= nil or lower_name:match("%.mov$") ~= nil or lower_name:match("%.mkv$") ~= nil end function list_files(path) local files = {} if path == nil or path == "" then return files end local search = path .. "\\*" local data = ffi.new("WIN32_FIND_DATAA[1]") local handle = ffi.C.FindFirstFileA(search, data) if handle == INVALID_HANDLE_VALUE then return files end repeat local name = ffi.string(data[0].cFileName) if name ~= "." and name ~= ".." and is_supported_video(name) then table.insert(files, path .. "\\" .. name) end until ffi.C.FindNextFileA(handle, data) == 0 ffi.C.FindClose(handle) -- File names contain date and time, so alphabetical order is intentional. table.sort(files) return files end ------------------------------------------------------------ -- Player ------------------------------------------------------------ function play_current() if not active then return end if #playlist == 0 then return end if index < 1 or index > #playlist then index = 1 end local file = playlist[index] local source = obs.obs_get_source_by_name(source_name) if source == nil then return end local settings = obs.obs_source_get_settings(source) obs.obs_data_set_bool(settings, "is_local_file", true) obs.obs_data_set_string(settings, "local_file", file) obs.obs_source_update(source, settings) obs.obs_data_release(settings) if obs.obs_source_media_restart ~= nil then obs.obs_source_media_restart(source) end obs.obs_source_release(source) end function next_video() if #playlist == 0 then return end index = index + 1 if index > #playlist then index = 1 end play_current() end ------------------------------------------------------------ -- Media Source events ------------------------------------------------------------ function media_ended(cd) next_video() end function source_active(cd) active = true playlist = list_files(folder_path) index = 1 play_current() end function source_inactive(cd) active = false end ------------------------------------------------------------ -- Source connection management ------------------------------------------------------------ function disconnect_source() if connected_source_name == nil or connected_source_name == "" then return end local source = obs.obs_get_source_by_name(connected_source_name) if source ~= nil then local signal_handler = obs.obs_source_get_signal_handler(source) obs.signal_handler_disconnect(signal_handler, "media_ended", media_ended) obs.signal_handler_disconnect(signal_handler, "activate", source_active) obs.signal_handler_disconnect(signal_handler, "deactivate", source_inactive) obs.obs_source_release(source) end connected_source_name = "" end function connect_source() disconnect_source() if source_name == nil or source_name == "" then active = false return false end local source = obs.obs_get_source_by_name(source_name) if source == nil then active = false return false end local signal_handler = obs.obs_source_get_signal_handler(source) obs.signal_handler_connect(signal_handler, "media_ended", media_ended) obs.signal_handler_connect(signal_handler, "activate", source_active) obs.signal_handler_connect(signal_handler, "deactivate", source_inactive) connected_source_name = source_name if obs.obs_source_active ~= nil then active = obs.obs_source_active(source) else active = true end obs.obs_source_release(source) return true end ------------------------------------------------------------ -- Initialization after OBS has loaded ------------------------------------------------------------ function initialize_player() playlist = list_files(folder_path) if not connect_source() then return end if #playlist == 0 then return end index = 1 if active then play_current() end end ------------------------------------------------------------ -- OBS frontend events ------------------------------------------------------------ function on_frontend_event(event) if event == obs.OBS_FRONTEND_EVENT_FINISHED_LOADING then initialize_player() elseif event == obs.OBS_FRONTEND_EVENT_REPLAY_BUFFER_SAVED then playlist = list_files(folder_path) if index > #playlist then index = 1 end elseif event == obs.OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED then initialize_player() end end ------------------------------------------------------------ -- UI ------------------------------------------------------------ function fill_media_sources(list) obs.obs_property_list_clear(list) obs.obs_property_list_add_string(list, "Select Media Source", "") 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) local name = obs.obs_source_get_name(source) if source_id == "ffmpeg_source" then obs.obs_property_list_add_string(list, name, name) end end obs.source_list_release(sources) end end function refresh_sources_button(props, property) local list = obs.obs_properties_get(props, "source_name") if list ~= nil then fill_media_sources(list) end return true end function script_properties() local props = obs.obs_properties_create() local source_list = obs.obs_properties_add_list( props, "source_name", "Media Source", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING ) fill_media_sources(source_list) obs.obs_properties_add_button( props, "refresh_sources", "Refresh Media Source List", refresh_sources_button ) obs.obs_properties_add_path( props, "folder_path", "Replay Folder", obs.OBS_PATH_DIRECTORY, "", nil ) return props end ------------------------------------------------------------ -- Settings ------------------------------------------------------------ function script_defaults(settings) obs.obs_data_set_default_string(settings, "source_name", "") obs.obs_data_set_default_string(settings, "folder_path", "") end function script_update(settings) source_name = obs.obs_data_get_string(settings, "source_name") folder_path = obs.obs_data_get_string(settings, "folder_path") -- This also supports changing the source manually after OBS has started. initialize_player() end ------------------------------------------------------------ -- Load and unload ------------------------------------------------------------ function script_load(settings) source_name = obs.obs_data_get_string(settings, "source_name") folder_path = obs.obs_data_get_string(settings, "folder_path") obs.obs_frontend_add_event_callback(on_frontend_event) -- Immediate attempt for manually loaded scripts. -- OBS_FRONTEND_EVENT_FINISHED_LOADING reconnects everything on startup. initialize_player() end function script_unload() disconnect_source() if obs.obs_frontend_remove_event_callback ~= nil then obs.obs_frontend_remove_event_callback(on_frontend_event) end end