diff --git a/.clang-format b/.clang-format index c18a977..bb054ca 100644 --- a/.clang-format +++ b/.clang-format @@ -11,12 +11,12 @@ AlignTrailingComments: true AllowAllArgumentsOnNextLine: false AllowAllConstructorInitializersOnNextLine: false AllowAllParametersOfDeclarationOnNextLine: false -AllowShortBlocksOnASingleLine: false -AllowShortCaseLabelsOnASingleLine: false -AllowShortFunctionsOnASingleLine: Inline -AllowShortIfStatementsOnASingleLine: false -AllowShortLambdasOnASingleLine: Inline -AllowShortLoopsOnASingleLine: false +AllowShortBlocksOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: true +AllowShortIfStatementsOnASingleLine: WithoutElse +AllowShortLambdasOnASingleLine: true +AllowShortLoopsOnASingleLine: true AlwaysBreakAfterDefinitionReturnType: None AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: false @@ -115,10 +115,10 @@ AlignConsecutiveMacros: Enabled: true AcrossEmptyLines: false AcrossComments: true -AllowShortBlocksOnASingleLine: Never +AllowShortBlocksOnASingleLine: true AllowShortEnumsOnASingleLine: false AllowShortFunctionsOnASingleLine: Empty -AllowShortIfStatementsOnASingleLine: Never +AllowShortIfStatementsOnASingleLine: true AllowShortLambdasOnASingleLine: None AttributeMacros: ['__unused', '__autoreleasing', '_Nonnull', '__bridge'] BitFieldColonSpacing: Both diff --git a/CMakeLists.txt b/CMakeLists.txt index 953e3db..4cf7582 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,16 +39,14 @@ if(ENABLE_QT) AUTORCC ON) endif() -set(SOURCES - src/main.cpp - src/input_source.cpp - src/utils.cpp -) -target_sources(${CMAKE_PROJECT_NAME} PRIVATE ${SOURCES}) - -# Include SDL3 headers +file(GLOB_RECURSE SOURCES "src/*.cpp") +file(GLOB_RECURSE HEADERS "src/*.hpp") + set_target_properties_plugin(${CMAKE_PROJECT_NAME} PROPERTIES OUTPUT_NAME ${_name}) +target_sources(${CMAKE_PROJECT_NAME} PRIVATE ${SOURCES} ${HEADERS}) target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + +# Add third-party dependencies target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE SDL3::SDL3) target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE DuckDB::duckdb) diff --git a/Makefile b/Makefile index e9cd080..9ad6548 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ configure: cmake --preset linux-x86_64 -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=lib/x86_64-linux-gnu build: + cmake --preset linux-x86_64 -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=lib/x86_64-linux-gnu cmake --build --preset linux-x86_64 # Build the project diff --git a/irec/aligner.py b/irec/aligner.py index c05d3a8..eed4430 100644 --- a/irec/aligner.py +++ b/irec/aligner.py @@ -72,10 +72,11 @@ def viz( Run the video and display the inputs at the bottom of the screen Pressing 'q' will exit the video Pressing space will pause the video - Pressing 'j' will go back 1 frame - Pressing 'k' will go forward 1 frame - Pressing 'l' will go forward 10 frames - Pressing 'h' will go back 10 frames + When paused: + 'j' will go back 1 frame + 'k' will go forward 1 frame + 'l' will go forward 10 frames + 'h' will go back 10 frames """ # Load video @@ -87,27 +88,35 @@ def viz( df = pl.read_parquet(path_inputs) N = df.shape[0] + + # Store frames in memory for backward seeking + frames = [] current_frame = 0 paused = False - def get_frame(cap, frame_number): - """Get the current frame of the video capture""" - cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number) - ret, frame = cap.read() - if not ret: - print("Failed to retrieve frame.") - return None - return frame - - while cap.isOpened(): - frame = get_frame(cap, current_frame) - frame_data = df.row(current_frame, named=True) - if frame is None: + def read_up_to_frame(target_frame): + """Read frames sequentially until reaching target_frame""" + nonlocal frames + while len(frames) <= target_frame: + ret, frame = cap.read() + if not ret: + return False + frames.append(frame) + return True + + while True: + # Ensure we have the current frame + if not read_up_to_frame(current_frame): break + + frame = frames[current_frame] + frame_data = df.row(current_frame, named=True) + if current_frame < offset: current_frame = offset continue + # Resize and create display frame frame = cv2.resize(frame, (800, 600)) new_width = frame.shape[1] + 200 frame_show = np.zeros((frame.shape[0], new_width, 3), dtype=np.uint8) @@ -133,16 +142,18 @@ def get_frame(cap, frame_number): break elif key == ord(" "): paused = not paused - elif key == ord("j") and current_frame > 0: - current_frame -= 1 - elif key == ord("k") and current_frame < N - 1: - current_frame += 1 - elif key == ord("l") and current_frame < N - 10: - current_frame += 10 - elif key == ord("h") and current_frame > 9: - current_frame -= 10 - if not paused: + # Only allow frame navigation when paused + if paused: + if key == ord("j") and current_frame > 0: + current_frame -= 1 + elif key == ord("k") and current_frame < N - 1: + current_frame += 1 + elif key == ord("l") and current_frame < N - 10: + current_frame += 10 + elif key == ord("h") and current_frame > 9: + current_frame -= 10 + elif not paused: current_frame += 1 # Cleanup diff --git a/src/device/gamepad.cpp b/src/device/gamepad.cpp new file mode 100644 index 0000000..685258d --- /dev/null +++ b/src/device/gamepad.cpp @@ -0,0 +1,208 @@ +#include "gamepad.hpp" +#include +#include + +constexpr int16_t AXIS_DEADZONE = 0; + +std::string axis_to_string(SDL_GamepadAxis axis) +{ + switch (axis) { + case SDL_GAMEPAD_AXIS_LEFTX: return "LEFTX"; + case SDL_GAMEPAD_AXIS_LEFTY: return "LEFTY"; + case SDL_GAMEPAD_AXIS_RIGHTX: return "RIGHTX"; + case SDL_GAMEPAD_AXIS_RIGHTY: return "RIGHTY"; + case SDL_GAMEPAD_AXIS_LEFT_TRIGGER: return "LEFT_TRIGGER"; + case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER: return "RIGHT_TRIGGER"; + default: return "UNKNOWN"; + } +} + +std::string button_to_string(SDL_GamepadButton button) +{ + switch (button) { + case SDL_GAMEPAD_BUTTON_SOUTH: return "SOUTH"; + case SDL_GAMEPAD_BUTTON_EAST: return "EAST"; + case SDL_GAMEPAD_BUTTON_WEST: return "WEST"; + case SDL_GAMEPAD_BUTTON_NORTH: return "NORTH"; + case SDL_GAMEPAD_BUTTON_BACK: return "BACK"; + case SDL_GAMEPAD_BUTTON_GUIDE: return "GUIDE"; + case SDL_GAMEPAD_BUTTON_START: return "START"; + case SDL_GAMEPAD_BUTTON_LEFT_STICK: return "LEFT_STICK"; + case SDL_GAMEPAD_BUTTON_RIGHT_STICK: return "RIGHT_STICK"; + case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER: return "LEFT_SHOULDER"; + case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: return "RIGHT_SHOULDER"; + case SDL_GAMEPAD_BUTTON_DPAD_UP: return "DPAD_UP"; + case SDL_GAMEPAD_BUTTON_DPAD_DOWN: return "DPAD_DOWN"; + case SDL_GAMEPAD_BUTTON_DPAD_LEFT: return "DPAD_LEFT"; + case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: return "DPAD_RIGHT"; + case SDL_GAMEPAD_BUTTON_MISC1: return "MISC1"; + case SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1: return "RIGHT_PADDLE1"; + case SDL_GAMEPAD_BUTTON_LEFT_PADDLE1: return "LEFT_PADDLE1"; + case SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2: return "RIGHT_PADDLE2"; + case SDL_GAMEPAD_BUTTON_LEFT_PADDLE2: return "LEFT_PADDLE2"; + case SDL_GAMEPAD_BUTTON_TOUCHPAD: return "TOUCHPAD"; + case SDL_GAMEPAD_BUTTON_MISC2: return "MISC2"; + case SDL_GAMEPAD_BUTTON_MISC3: return "MISC3"; + case SDL_GAMEPAD_BUTTON_MISC4: return "MISC4"; + case SDL_GAMEPAD_BUTTON_MISC5: return "MISC5"; + case SDL_GAMEPAD_BUTTON_MISC6: return "MISC6"; + case SDL_GAMEPAD_BUTTON_COUNT: return "MAX"; + default: return "UNKNOWN"; + } +} + +void GamepadDevice::add_gamepad(SDL_JoystickID joystickid) +{ + SDL_Gamepad *gamepad = SDL_OpenGamepad(joystickid); + if (gamepad) { + m_gamepads.push_back(gamepad); + } else { + std::cerr << "Failed to open gamepad: " << SDL_GetError() << std::endl; + } +} + +void GamepadDevice::remove_gamepad(SDL_JoystickID joystickid) +{ + int i = get_gamepad_idx(joystickid); + if (m_gamepads[i]) { + SDL_CloseGamepad(m_gamepads[i]); + m_gamepads[i] = nullptr; + } +} + +int GamepadDevice::get_gamepad_idx(SDL_JoystickID joystickid) +{ + for (size_t i = 0; i < m_gamepads.size(); ++i) { + if (m_gamepads[i]) { + SDL_Joystick *joystick = SDL_GetGamepadJoystick(m_gamepads[i]); + if (joystick && SDL_GetJoystickID(joystick) == joystickid) { return static_cast(i); } + } + } + return -1; +} + +SDL_Gamepad *GamepadDevice::active_gamepad() +{ + for (auto gamepad : m_gamepads) { + if (gamepad) { return gamepad; } + } + return nullptr; +} + +GamepadDevice::GamepadDevice() +{ + /* Init SDL, see SDL/test/testcontroller.c */ + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "1"); + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1"); + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1"); + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_STEAM, "1"); + SDL_SetHint(SDL_HINT_JOYSTICK_ROG_CHAKRAM, "1"); + SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); + SDL_SetHint(SDL_HINT_JOYSTICK_LINUX_DEADZONES, "1"); + + /* Enable input debug logging */ + SDL_SetLogPriority(SDL_LOG_CATEGORY_INPUT, SDL_LOG_PRIORITY_DEBUG); + if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD)) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't initialize SDL: %s\n", SDL_GetError()); + return; + } + + SDL_AddGamepadMappingsFromFile("gamecontrollerdb.txt"); + +#ifdef DEBUG + int count = 0; + char **mappings = SDL_GetGamepadMappings(&count); + int map_i; + SDL_Log("Supported mappings:\n"); + for (map_i = 0; map_i < count; ++map_i) { SDL_Log("\t%s\n", mappings[map_i]); } + SDL_Log("\n"); + SDL_free(mappings); +#endif + + /* The following delay is necessary to avoid a crash on Linux + The crash occurs in a particular setting, on Ubuntu: + - if the gamepad is connected to the computer before obs is launched + - if the obs instance has input-overlay and input-rec installed + - if the obs instance has one source for both input-overlay and input-rec + Then opening OBS will segfault. The issue seems to be about SDL initialization + and gamepad detection. A delay >1.5s avoid the crash. + + I don't think the issue comes from input-rec, because this crash has been + reported on the input-overlay repo: https://github.com/univrsal/input-overlay/issues/426 + The issue also occurs when using the obs input-overlay plugin only, without input-rec. + */ + std::this_thread::sleep_for(std::chrono::milliseconds(2000)); + + // Init gamepads + int count = 0; + SDL_JoystickID *joystick_ids = SDL_GetGamepads(&count); + if (joystick_ids) { + for (int i = 0; i < count; ++i) { + add_gamepad(joystick_ids[i]); + std::cout << "Gamepad added: " << joystick_ids[i] << std::endl; + } + SDL_free(joystick_ids); + } else { + std::cerr << "Failed to get gamepads: " << SDL_GetError() << std::endl; + } +} + +GamepadDevice::~GamepadDevice() +{ + for (auto gamepad : m_gamepads) { SDL_CloseGamepad(gamepad); } + SDL_Quit(); +} + +void GamepadDevice::write_header(InputWriter &writer) +{ + writer.begin_header(); + for (int i = 0; i < SDL_GAMEPAD_BUTTON_TOUCHPAD; ++i) + writer.append_header(false, button_to_string((SDL_GamepadButton)i)); + for (int i = 0; i < SDL_GAMEPAD_AXIS_COUNT; ++i) + writer.append_header(static_cast(0), axis_to_string((SDL_GamepadAxis)i)); + writer.end_header(); +} + +void GamepadDevice::write_state(InputWriter &writer) +{ + SDL_Gamepad *gamepad = active_gamepad(); + if (gamepad) { + writer.begin_row(); + for (int i = 0; i < SDL_GAMEPAD_BUTTON_TOUCHPAD; ++i) { + const SDL_GamepadButton button = (SDL_GamepadButton)i; + const bool pressed = SDL_GetGamepadButton(gamepad, button) == true; + writer.append_row(pressed); + } + + for (int i = 0; i < SDL_GAMEPAD_AXIS_COUNT; ++i) { + const SDL_GamepadAxis axis = (SDL_GamepadAxis)i; + int16_t value = SDL_GetGamepadAxis(gamepad, axis); + // TODO: Use dead zone? + // value = (value < AXIS_DEADZONE && value > -AXIS_DEADZONE) ? 0 : value; + writer.append_row(value); + } + writer.end_row(); + } +} + +void GamepadDevice::loop(InputWriter &writer) +{ + SDL_Event event; + SDL_PumpEvents(); + while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_EVENT_FIRST, SDL_EVENT_LAST) == 1) { + switch (event.type) { + case SDL_EVENT_GAMEPAD_ADDED: + add_gamepad(event.gdevice.which); + std::cout << "Gamepad added" << std::endl; + break; + case SDL_EVENT_GAMEPAD_REMOVED: + remove_gamepad(event.gdevice.which); + std::cout << "Gamepad removed" << std::endl; + break; + default: break; + } + } + write_state(writer); + // TODO: delay should be handled by the writer, not the device + SDL_Delay(2); +} diff --git a/src/device/gamepad.hpp b/src/device/gamepad.hpp new file mode 100644 index 0000000..4562c6b --- /dev/null +++ b/src/device/gamepad.hpp @@ -0,0 +1,23 @@ +#pragma once +#include +#include +#include "input_device.hpp" +#include "writer/input_writer.hpp" + +class GamepadDevice : public InputDevice { +private: + std::vector m_gamepads; + + void add_gamepad(SDL_JoystickID joystickid); + void remove_gamepad(SDL_JoystickID joystickid); + int get_gamepad_idx(SDL_JoystickID joystickid); + SDL_Gamepad *active_gamepad(); + +public: + GamepadDevice(); + ~GamepadDevice() override; + + void write_header(InputWriter &writer) override; + void write_state(InputWriter &writer) override; + void loop(InputWriter &writer) override; +}; \ No newline at end of file diff --git a/src/device/input_device.hpp b/src/device/input_device.hpp new file mode 100644 index 0000000..5dc58ba --- /dev/null +++ b/src/device/input_device.hpp @@ -0,0 +1,10 @@ +#pragma once + +class InputWriter; // necessary forward declaration +class InputDevice { +public: + virtual ~InputDevice() = default; + virtual void write_header(InputWriter &writer) = 0; + virtual void write_state(InputWriter &writer) = 0; + virtual void loop(InputWriter &writer) = 0; +}; \ No newline at end of file diff --git a/src/input_source.cpp b/src/input_source.cpp deleted file mode 100644 index 3a7e7e7..0000000 --- a/src/input_source.cpp +++ /dev/null @@ -1,301 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "input_source.hpp" -#include "plugin-support.h" - -namespace fs = std::filesystem; - -constexpr int16_t AXIS_DEADZONE = 0; - -gamepad_manager::gamepad_manager() : m_timer{} -{ - /* Init SDL, see SDL/test/testcontroller.c */ - SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "1"); - SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1"); - SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1"); - SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_STEAM, "1"); - SDL_SetHint(SDL_HINT_JOYSTICK_ROG_CHAKRAM, "1"); - SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); - SDL_SetHint(SDL_HINT_JOYSTICK_LINUX_DEADZONES, "1"); - - /* Enable input debug logging */ - SDL_SetLogPriority(SDL_LOG_CATEGORY_INPUT, SDL_LOG_PRIORITY_DEBUG); - if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD)) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't initialize SDL: %s\n", SDL_GetError()); - return; - } - - SDL_AddGamepadMappingsFromFile("gamecontrollerdb.txt"); - -#ifdef DEBUG - int count = 0; - char **mappings = SDL_GetGamepadMappings(&count); - int map_i; - SDL_Log("Supported mappings:\n"); - for (map_i = 0; map_i < count; ++map_i) { - SDL_Log("\t%s\n", mappings[map_i]); - } - SDL_Log("\n"); - SDL_free(mappings); -#endif - - init_gamepads(); -} - -void gamepad_manager::init_gamepads() -{ - int count = 0; - SDL_JoystickID *joystick_ids = SDL_GetGamepads(&count); - if (joystick_ids) { - for (int i = 0; i < count; ++i) { - add_gamepad(joystick_ids[i]); - std::cout << "Gamepad added: " << joystick_ids[i] << std::endl; - } - SDL_free(joystick_ids); - } else { - std::cerr << "Failed to get gamepads: " << SDL_GetError() << std::endl; - } -} - -void gamepad_manager::add_gamepad(SDL_JoystickID joystickid) -{ - SDL_Gamepad *gamepad = SDL_OpenGamepad(joystickid); - if (gamepad) { - m_gamepads.push_back(gamepad); - } else { - std::cerr << "Failed to open gamepad: " << SDL_GetError() << std::endl; - } -} - -int gamepad_manager::get_gamepad_idx(SDL_JoystickID joystickid) -{ - for (size_t i = 0; i < m_gamepads.size(); ++i) { - if (m_gamepads[i]) { - SDL_Joystick *joystick = SDL_GetGamepadJoystick(m_gamepads[i]); - if (joystick && SDL_GetJoystickID(joystick) == joystickid) { - return static_cast(i); - } - } - } - return -1; -} - -void gamepad_manager::remove_gamepad(SDL_JoystickID joystickid) -{ - int i = get_gamepad_idx(joystickid); - if (m_gamepads[i]) { - SDL_CloseGamepad(m_gamepads[i]); - m_gamepads[i] = nullptr; - } -} - -SDL_Gamepad *gamepad_manager::active_gamepad() -{ - for (auto gamepad : m_gamepads) { - if (gamepad) { - return gamepad; - } - } - return nullptr; -} - -void gamepad_manager::save_gamepad_state() -{ - SDL_Gamepad *gamepad = active_gamepad(); - // Print number of gamepads - if (gamepad) { - auto dt = m_timer.elapsed(); - if (!dt) - return; - - int i; - m_file << *dt << ","; - for (i = 0; i < SDL_GAMEPAD_BUTTON_TOUCHPAD; ++i) { - const SDL_GamepadButton button = (SDL_GamepadButton)i; - const bool pressed = SDL_GetGamepadButton(gamepad, button) == true; - m_file << pressed << ","; - } - - for (i = 0; i < SDL_GAMEPAD_AXIS_COUNT; ++i) { - const SDL_GamepadAxis axis = (SDL_GamepadAxis)i; - int16_t value = SDL_GetGamepadAxis(gamepad, axis); - value = (value < AXIS_DEADZONE && value > -AXIS_DEADZONE) ? 0 : value; - m_file << value << ","; - } - - m_file << std::endl; - } -} - -gamepad_manager::~gamepad_manager() -{ - m_running = false; - if (m_thread_loop.joinable()) { - m_thread_loop.join(); - } - for (auto gamepad : m_gamepads) { - SDL_CloseGamepad(gamepad); - } - m_file.close(); - SDL_Quit(); -} - -void gamepad_manager::loop() -{ - SDL_Event event; - SDL_PumpEvents(); - while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_EVENT_FIRST, SDL_EVENT_LAST) == 1) { - switch (event.type) { - case SDL_EVENT_GAMEPAD_ADDED: - add_gamepad(event.gdevice.which); - std::cout << "Gamepad added" << std::endl; - break; - case SDL_EVENT_GAMEPAD_REMOVED: - remove_gamepad(event.gdevice.which); - std::cout << "Gamepad removed" << std::endl; - break; - default: - break; - } - } - save_gamepad_state(); - SDL_Delay(2); -} - -void gamepad_manager::prepare_recording() -{ - // Create tmp file with random name - m_file_path = fs::temp_directory_path() / - fs::path("obs_input_rec_" + std::to_string(std::random_device{}()) + ".csv"); - m_file.open(m_file_path, std::ios::trunc); - - if (!m_file.is_open()) { - std::cerr << "Failed to open file: " << m_file_path << std::endl; - return; - } - - m_file << "time,"; - for (int i = 0; i < SDL_GAMEPAD_BUTTON_TOUCHPAD; ++i) { - m_file << button_to_string((SDL_GamepadButton)i) << ","; - } - for (int i = 0; i < SDL_GAMEPAD_AXIS_COUNT; ++i) { - m_file << axis_to_string((SDL_GamepadAxis)i) << ","; - } - m_file << std::endl; -} - -void gamepad_manager::start_recording() -{ - m_timer.start(); - m_running = true; - - m_thread_loop = std::thread([this]() { - while (m_running) { - loop(); - } - }); -} - -void gamepad_manager::stop_recording() -{ - m_timer.stop(); - m_running = false; - if (m_thread_loop.joinable()) - m_thread_loop.join(); -} - -void gamepad_manager::close_recording(std::string recording_path) -{ - // Close file and move it to the recording path with a .csv extension - m_file.close(); - fs::path recording_csv = fs::path(recording_path).replace_extension(".csv"); - fs::rename(m_file_path, recording_csv); -} - -class rec_source { -private: - obs_data_t *m_settings; - obs_source_t *m_source; - gamepad_manager m_gamepad_manager; - -public: - rec_source(obs_data_t *settings, obs_source_t *source) - : m_settings{settings}, - m_source{source}, - m_gamepad_manager{} - { - - obs_frontend_add_event_callback( - [](enum obs_frontend_event event, void *private_data) { - gamepad_manager *current_gpm = static_cast(private_data); - switch (event) { - case OBS_FRONTEND_EVENT_RECORDING_STARTING: { - obs_log(LOG_INFO, "OBS_FRONTEND_EVENT_RECORDING_STARTING received"); - current_gpm->prepare_recording(); - break; - } - case OBS_FRONTEND_EVENT_RECORDING_STARTED: { - obs_log(LOG_INFO, "OBS_FRONTEND_EVENT_RECORDING_STARTED received"); - current_gpm->start_recording(); - break; - } - case OBS_FRONTEND_EVENT_RECORDING_STOPPING: { - obs_log(LOG_INFO, "OBS_FRONTEND_EVENT_RECORDING_STOPPING received"); - current_gpm->stop_recording(); - break; - } - case OBS_FRONTEND_EVENT_RECORDING_STOPPED: { - obs_log(LOG_INFO, "OBS_FRONTEND_EVENT_RECORDING_STOPPED received"); - // Get last recording path - std::string recording_path{obs_frontend_get_last_recording()}; - current_gpm->close_recording(recording_path); - break; - } - default: - break; - } - }, - // pass gamepad_manager instance as private_data - &m_gamepad_manager); - } - - void tick(float seconds) { UNUSED_PARAMETER(seconds); } -}; - -bool initialize_rec_source() -{ - obs_source_info source_info = {}; - source_info.id = "input_recording_source"; - source_info.type = OBS_SOURCE_TYPE_INPUT; - source_info.output_flags = OBS_SOURCE_VIDEO; - source_info.icon_type = OBS_ICON_TYPE_GAME_CAPTURE; - source_info.get_name = [](void *) { - return obs_module_text("InputRecording"); - ; - }; - source_info.get_width = [](void *) -> uint32_t { - return 0; - }; - source_info.get_height = [](void *) -> uint32_t { - return 0; - }; - source_info.create = [](obs_data_t *settings, obs_source_t *source) -> void * { - return static_cast(new rec_source(settings, source)); - }; - source_info.destroy = [](void *data) { - delete static_cast(data); - }; - source_info.video_tick = [](void *data, float seconds) { - static_cast(data)->tick(seconds); - }; - obs_register_source(&source_info); - return true; -} diff --git a/src/input_source.hpp b/src/input_source.hpp deleted file mode 100644 index bae129c..0000000 --- a/src/input_source.hpp +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include "utils.hpp" - -class gamepad_manager { -private: - std::vector m_gamepads; - std::ofstream m_file; - std::filesystem::path m_file_path; - rec_timer m_timer; - std::atomic m_running{false}; - std::thread m_thread_loop; - void init_gamepads(); - void add_gamepad(SDL_JoystickID joystickid); - void remove_gamepad(SDL_JoystickID joystickid); - int get_gamepad_idx(SDL_JoystickID joystickid); - SDL_Gamepad *active_gamepad(); - void save_gamepad_state(); - void loop(); - -public: - gamepad_manager(); - ~gamepad_manager(); - void prepare_recording(); - void start_recording(); - void stop_recording(); - void close_recording(std::string recording_path); -}; - -bool initialize_rec_source(); diff --git a/src/main.cpp b/src/main.cpp index ef8b529..e98d589 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -22,11 +22,9 @@ with this program. If not, see #include #include #include -#include #include "plugin-support.h" -#include "input_source.hpp" -#include "utils.hpp" +#include "obs_source.hpp" OBS_DECLARE_MODULE() OBS_MODULE_USE_DEFAULT_LOCALE(PLUGIN_NAME, "en-US") @@ -36,12 +34,8 @@ bool obs_module_load(void) // Dummy duckdb code, open a database and close it duckdb_database db; duckdb_connection con; - if (duckdb_open(NULL, &db) == DuckDBError) { - obs_log(LOG_ERROR, "Failed to open database"); - } - if (duckdb_connect(db, &con) == DuckDBError) { - obs_log(LOG_ERROR, "Failed to connect to database"); - } + if (duckdb_open(NULL, &db) == DuckDBError) obs_log(LOG_ERROR, "Failed to open database"); + if (duckdb_connect(db, &con) == DuckDBError) obs_log(LOG_ERROR, "Failed to connect to database"); duckdb_query(con, "SELECT 42", NULL); duckdb_disconnect(&con); duckdb_close(&db); @@ -52,10 +46,8 @@ bool obs_module_load(void) } obs_log(LOG_INFO, "input-rec (version %s) loaded successfully", PLUGIN_VERSION); + obs_log(LOG_INFO, "input-rec version UwU"); return true; } -void obs_module_unload(void) -{ - obs_log(LOG_INFO, "input-rec unloaded"); -} +void obs_module_unload(void) { obs_log(LOG_INFO, "input-rec unloaded"); } \ No newline at end of file diff --git a/src/obs_source.cpp b/src/obs_source.cpp new file mode 100644 index 0000000..9dac415 --- /dev/null +++ b/src/obs_source.cpp @@ -0,0 +1,78 @@ +#include +#include + +#include "device/input_device.hpp" +#include "device/gamepad.hpp" +#include "writer/input_writer.hpp" +#include "writer/csv.hpp" +#include "obs_source.hpp" +#include "plugin-support.h" + +class RecSource { +private: + obs_data_t *m_settings; + obs_source_t *m_source; + std::unique_ptr m_input_writer; + +public: + RecSource(obs_data_t *settings, obs_source_t *source) + : m_settings{settings}, + m_source{source}, + m_input_writer{std::make_unique(std::make_unique())} + { + obs_frontend_add_event_callback( + [](enum obs_frontend_event event, void *private_data) { + InputWriter *current_writer = static_cast(private_data); + switch (event) { + case OBS_FRONTEND_EVENT_RECORDING_STARTING: { + obs_log(LOG_INFO, "OBS_FRONTEND_EVENT_RECORDING_STARTING received"); + current_writer->prepare_recording(); + break; + } + case OBS_FRONTEND_EVENT_RECORDING_STARTED: { + obs_log(LOG_INFO, "OBS_FRONTEND_EVENT_RECORDING_STARTED received"); + current_writer->start_recording(); + break; + } + case OBS_FRONTEND_EVENT_RECORDING_STOPPING: { + obs_log(LOG_INFO, "OBS_FRONTEND_EVENT_RECORDING_STOPPING received"); + current_writer->stop_recording(); + break; + } + case OBS_FRONTEND_EVENT_RECORDING_STOPPED: { + obs_log(LOG_INFO, "OBS_FRONTEND_EVENT_RECORDING_STOPPED received"); + // Get last recording path + std::string recording_path{obs_frontend_get_last_recording()}; + current_writer->close_recording(recording_path); + break; + } + default: break; + } + }, + m_input_writer.get()); + } + + void tick(float seconds) { UNUSED_PARAMETER(seconds); } +}; + +bool initialize_rec_source() +{ + obs_source_info source_info = {}; + source_info.id = "input_recording_source"; + source_info.type = OBS_SOURCE_TYPE_INPUT; + source_info.output_flags = OBS_SOURCE_VIDEO; + source_info.icon_type = OBS_ICON_TYPE_GAME_CAPTURE; + source_info.get_name = [](void *) { + return obs_module_text("InputRecording"); + ; + }; + source_info.get_width = [](void *) -> uint32_t { return 0; }; + source_info.get_height = [](void *) -> uint32_t { return 0; }; + source_info.create = [](obs_data_t *settings, obs_source_t *source) -> void * { + return static_cast(new RecSource(settings, source)); + }; + source_info.destroy = [](void *data) { delete static_cast(data); }; + source_info.video_tick = [](void *data, float seconds) { static_cast(data)->tick(seconds); }; + obs_register_source(&source_info); + return true; +} diff --git a/src/obs_source.hpp b/src/obs_source.hpp new file mode 100644 index 0000000..19e6962 --- /dev/null +++ b/src/obs_source.hpp @@ -0,0 +1,2 @@ +#pragma once +bool initialize_rec_source(); diff --git a/src/utils.cpp b/src/utils.cpp deleted file mode 100644 index b6dcb5c..0000000 --- a/src/utils.cpp +++ /dev/null @@ -1,116 +0,0 @@ -#include "utils.hpp" - -rec_timer::rec_timer() : m_start(std::chrono::high_resolution_clock::now()) -{ - std::cout << "Timer created" << std::endl; -} - -rec_timer::~rec_timer() -{ - std::cout << "Timer destroyed" << std::endl; -} - -void rec_timer::start() -{ - m_start = std::chrono::high_resolution_clock::now(); - m_running = true; - - std::cout << "Timer started" << std::endl; -} - -void rec_timer::stop() -{ - m_running = false; -} - -std::optional rec_timer::elapsed() -{ - if (!m_running) - return std::nullopt; - auto now = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast(now - m_start); - auto dt = duration.count(); - return dt; -} - -std::string axis_to_string(SDL_GamepadAxis axis) -{ - switch (axis) { - case SDL_GAMEPAD_AXIS_LEFTX: - return "LEFTX"; - case SDL_GAMEPAD_AXIS_LEFTY: - return "LEFTY"; - case SDL_GAMEPAD_AXIS_RIGHTX: - return "RIGHTX"; - case SDL_GAMEPAD_AXIS_RIGHTY: - return "RIGHTY"; - case SDL_GAMEPAD_AXIS_LEFT_TRIGGER: - return "LEFT_TRIGGER"; - case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER: - return "RIGHT_TRIGGER"; - default: - return "UNKNOWN"; - } -} - -std::string button_to_string(SDL_GamepadButton button) -{ - switch (button) { - case SDL_GAMEPAD_BUTTON_SOUTH: - return "SOUTH"; - case SDL_GAMEPAD_BUTTON_EAST: - return "EAST"; - case SDL_GAMEPAD_BUTTON_WEST: - return "WEST"; - case SDL_GAMEPAD_BUTTON_NORTH: - return "NORTH"; - case SDL_GAMEPAD_BUTTON_BACK: - return "BACK"; - case SDL_GAMEPAD_BUTTON_GUIDE: - return "GUIDE"; - case SDL_GAMEPAD_BUTTON_START: - return "START"; - case SDL_GAMEPAD_BUTTON_LEFT_STICK: - return "LEFT_STICK"; - case SDL_GAMEPAD_BUTTON_RIGHT_STICK: - return "RIGHT_STICK"; - case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER: - return "LEFT_SHOULDER"; - case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: - return "RIGHT_SHOULDER"; - case SDL_GAMEPAD_BUTTON_DPAD_UP: - return "DPAD_UP"; - case SDL_GAMEPAD_BUTTON_DPAD_DOWN: - return "DPAD_DOWN"; - case SDL_GAMEPAD_BUTTON_DPAD_LEFT: - return "DPAD_LEFT"; - case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: - return "DPAD_RIGHT"; - case SDL_GAMEPAD_BUTTON_MISC1: - return "MISC1"; - case SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1: - return "RIGHT_PADDLE1"; - case SDL_GAMEPAD_BUTTON_LEFT_PADDLE1: - return "LEFT_PADDLE1"; - case SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2: - return "RIGHT_PADDLE2"; - case SDL_GAMEPAD_BUTTON_LEFT_PADDLE2: - return "LEFT_PADDLE2"; - case SDL_GAMEPAD_BUTTON_TOUCHPAD: - return "TOUCHPAD"; - case SDL_GAMEPAD_BUTTON_MISC2: - return "MISC2"; - case SDL_GAMEPAD_BUTTON_MISC3: - return "MISC3"; - case SDL_GAMEPAD_BUTTON_MISC4: - return "MISC4"; - case SDL_GAMEPAD_BUTTON_MISC5: - return "MISC5"; - case SDL_GAMEPAD_BUTTON_MISC6: - return "MISC6"; - case SDL_GAMEPAD_BUTTON_COUNT: - return "MAX"; - default: - return "UNKNOWN"; - } -} diff --git a/src/utils.hpp b/src/utils.hpp deleted file mode 100644 index 01b74cf..0000000 --- a/src/utils.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include - -std::string axis_to_string(SDL_GamepadAxis axis); -std::string button_to_string(SDL_GamepadButton button); - -class rec_timer { -private: - std::chrono::high_resolution_clock::time_point m_start; - std::atomic m_running{false}; - -public: - rec_timer(); - ~rec_timer(); - void start(); - void stop(); - std::optional elapsed(); -}; diff --git a/src/writer/csv.cpp b/src/writer/csv.cpp new file mode 100644 index 0000000..6afed81 --- /dev/null +++ b/src/writer/csv.cpp @@ -0,0 +1,49 @@ +#include "csv.hpp" +#include +#include + +namespace fs = std::filesystem; + +CSVWriter::CSVWriter(std::unique_ptr device) : InputWriter{std::move(device)} {} + +void CSVWriter::prepare_recording() +{ + // Create tmp file with random name + m_file_path = fs::temp_directory_path() / + fs::path("obs_input_rec_" + std::to_string(std::random_device{}()) + ".csv"); + m_file.open(m_file_path, std::ios::trunc); + + if (!m_file.is_open()) { + std::cerr << "Failed to open file: " << m_file_path << std::endl; + return; + } + + // Write header + m_device->write_header(*this); +} + +void CSVWriter::close_recording(std::string recording_path) +{ + // Close file and move it to the recording path with a .csv extension + m_file.close(); + fs::path recording_csv = fs::path(recording_path).replace_extension(".csv"); + fs::rename(m_file_path, recording_csv); +} + +void CSVWriter::begin_header() { append_header(static_cast(0), "time"); } +void CSVWriter::end_header() { m_file << std::endl; } + +void CSVWriter::begin_row() +{ + auto dt = m_timer.elapsed(); + if (dt) m_file << *dt << ","; +} +void CSVWriter::end_row() { m_file << std::endl; } + +void CSVWriter::append_header(const bool &value, const std::string &name) { m_file << name << ","; } +void CSVWriter::append_header(const int16_t &value, const std::string &name) { m_file << name << ","; } +void CSVWriter::append_header(const int64_t &value, const std::string &name) { m_file << name << ","; } + +void CSVWriter::append_row(const bool &value) { m_file << value << ","; } +void CSVWriter::append_row(const int16_t &value) { m_file << value << ","; } +void CSVWriter::append_row(const int64_t &value) { m_file << value << ","; } diff --git a/src/writer/csv.hpp b/src/writer/csv.hpp new file mode 100644 index 0000000..68ccfb1 --- /dev/null +++ b/src/writer/csv.hpp @@ -0,0 +1,29 @@ +#pragma once +#include +#include +#include "input_writer.hpp" + +class CSVWriter : public InputWriter { +private: + std::ofstream m_file; + std::filesystem::path m_file_path; + +public: + CSVWriter(std::unique_ptr device); + + void prepare_recording() override; + void close_recording(std::string recording_path) override; + + void begin_header() override; + void end_header() override; + void begin_row() override; + void end_row() override; + + void append_header(const bool &value, const std::string &name) override; + void append_header(const int16_t &value, const std::string &name) override; + void append_header(const int64_t &value, const std::string &name) override; + + void append_row(const bool &value) override; + void append_row(const int16_t &value) override; + void append_row(const int64_t &value) override; +}; \ No newline at end of file diff --git a/src/writer/input_writer.hpp b/src/writer/input_writer.hpp new file mode 100644 index 0000000..128f5a3 --- /dev/null +++ b/src/writer/input_writer.hpp @@ -0,0 +1,117 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +#include "device/input_device.hpp" + +class RecTimer { +private: + std::chrono::high_resolution_clock::time_point m_start{}; + +public: + std::atomic running{false}; + void start() + { + m_start = std::chrono::high_resolution_clock::now(); + running = true; + } + + void stop() + { + // print elapsed time in min:sec:ms + auto dt = elapsed(); + if (dt) { + // Convert microseconds to appropriate units + auto ms = (*dt / 1000) % 1000; // Convert microseconds to milliseconds + auto s = (*dt / 1000000) % 60; // Convert microseconds to seconds + auto m = (*dt / 60000000); // Convert microseconds to minutes + + // Format with leading zeros and proper width + std::cout << "[input-rec] Elapsed time: " << std::setfill('0') << std::setw(2) << m << ":" + << std::setfill('0') << std::setw(2) << s << ":" << std::setfill('0') << std::setw(3) + << ms << std::endl; + } + + running = false; + } + + std::optional elapsed() + { + if (!running) return std::nullopt; + auto now = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(now - m_start); + auto dt = duration.count(); + return dt; + } +}; + +using SupportedTypes = std::variant; +class InputWriter { +protected: + RecTimer m_timer; + std::thread m_thread_loop; + /* dependency injection of an InputDevice + each InputWriter have a reference to an InputDevice + which they use to get the state of an input device */ + std::unique_ptr m_device; + +public: + InputWriter(std::unique_ptr device) : m_device{std::move(device)}, m_timer{} {} + + virtual ~InputWriter() = default; + + // Start/end a recording session + virtual void prepare_recording() = 0; + virtual void close_recording(std::string recording_path) = 0; + + // Start/Stop recording implementation are the same for all InputWriter + void start_recording() + { + m_timer.start(); + + m_thread_loop = std::thread([this]() { + while (m_timer.running) m_device->loop(*this); + }); + } + void stop_recording() + { + m_timer.stop(); + + if (m_thread_loop.joinable()) m_thread_loop.join(); + } + + // Start/end a header or row + virtual void begin_header() = 0; + virtual void end_header() = 0; + virtual void begin_row() = 0; + virtual void end_row() = 0; + + // Overloaded functions to append data + virtual void append_header(const bool &value, const std::string &name) = 0; + virtual void append_header(const int16_t &value, const std::string &name) = 0; + virtual void append_header(const int64_t &value, const std::string &name) = 0; + + virtual void append_row(const bool &value) = 0; + virtual void append_row(const int16_t &value) = 0; + virtual void append_row(const int64_t &value) = 0; + + /* The following functions are just there to check that the overloads of + append_header and append_row cover all the supported types, if it isn't the + case, the program will not compile */ + + void _append_header_check(const SupportedTypes &value, const std::string &name) + { + std::visit([this, &name](auto &&arg) { this->append_header(arg, name); }, value); + } + + void _append_row_check(const SupportedTypes &value) + { + std::visit([this](auto &&arg) { this->append_row(arg); }, value); + } +};