diff --git a/BUILD-FULL-CLEAN.cmd b/BUILD-FULL-CLEAN.cmd index 910127b..0ceee17 100644 --- a/BUILD-FULL-CLEAN.cmd +++ b/BUILD-FULL-CLEAN.cmd @@ -1,7 +1,7 @@ @echo off setlocal cd /d "%~dp0" -echo WARNING: this removes all local build caches and downloaded archives for r6.3. +echo WARNING: this removes all local build caches and downloaded archives for r6.4. echo Older EB Recorder folders are not touched. echo. powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\build-windows.ps1" -FullClean diff --git a/CHANGELOG.md b/CHANGELOG.md index eeb0386..9074d6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [0.3.2](release-notes/v0.3.2.md) + +Native OBS hotkeys. + +- Added OBS frontend hotkeys for starting and stopping EB Recorder. +- Hotkey bindings are loaded from the active OBS profile and refreshed on profile changes. +- Start hotkey reuses the same TOP EB and Live/VOD selection logic as the dialog. +- Stop hotkey uses the normal clean-stop path, including Matroska `BPS` metadata finalization. +- No global keyboard hooks or additional encoders are introduced. + ## [0.3.1](release-notes/v0.3.1.md) Audio-track and metadata update. diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b1b487..5a7cb2d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 4.2...4.3) -project(eb-recorder VERSION 0.3.1 LANGUAGES CXX) +project(eb-recorder VERSION 0.3.2 LANGUAGES CXX) find_package(libobs CONFIG REQUIRED) find_package(obs-frontend-api CONFIG REQUIRED) diff --git a/README.md b/README.md index 03eb6e4..d69fd46 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ No extra video encoder is created by EB Recorder. - Optional **automatic recording start with the EB stream**. - Automatic stop before the Enhanced Broadcasting encoder pipeline is torn down. - Manual Start / Stop remains available. +- Native OBS hotkeys for **EB Recorder: Start recording** and **EB Recorder: Stop recording**, configurable in **Settings → Hotkeys** and loaded per OBS profile. - English and Russian UI. - Does not modify Windows display mode, refresh rate, VRR, G-SYNC, NVIDIA settings, registry, services, power plans or PATH. diff --git a/README_RU.md b/README_RU.md index 8966053..f83572b 100644 --- a/README_RU.md +++ b/README_RU.md @@ -35,6 +35,7 @@ EB Recorder находит уже активные EB-кодировщики OBS - Опциональный **автоматический старт записи вместе с EB-трансляцией**. - Автоматическая остановка локальной записи до уничтожения EB encoder pipeline. - Ручные Start / Stop остаются доступными. +- Штатные горячие клавиши OBS **EB Recorder: Начать запись** и **EB Recorder: Остановить запись**, настраиваемые в **Настройки → Горячие клавиши** и загружаемые отдельно для каждого профиля OBS. - Русский и английский интерфейс. - Плагин не меняет режим дисплея Windows, частоту обновления, VRR, G-SYNC, настройки NVIDIA, реестр, службы, планы питания или PATH. diff --git a/VERSION.txt b/VERSION.txt index 9e11b32..d15723f 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -0.3.1 +0.3.2 diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 86ed64a..58a2701 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -1,6 +1,6 @@ EBRecorder.ToolsMenu="EB Recorder" -EBRecorder.Title="EB Recorder 0.3.1" -EBRecorder.Intro="Version 0.3.1 records the active highest-resolution Enhanced Broadcasting rendition to crash-resilient Matroska (MKV) by reusing the existing EB video and selected Live/VOD audio encoders. Recording can be started manually or automatically together with the EB stream." +EBRecorder.Title="EB Recorder 0.3.2" +EBRecorder.Intro="Version 0.3.2 records the active highest-resolution Enhanced Broadcasting rendition to crash-resilient Matroska (MKV) by reusing the existing EB video and selected Live/VOD audio encoders. Recording can be started manually or automatically together with the EB stream." EBRecorder.Status.None="No active Enhanced Broadcasting video encoders detected. Start an EB stream and refresh." EBRecorder.Status.Found="Detected %1 active Enhanced Broadcasting video encoder(s)." EBRecorder.Selected.None="Top EB stream: not available." @@ -47,3 +47,6 @@ EBRecorder.Refresh="Refresh" EBRecorder.StartRecording="Start recording" EBRecorder.StopRecording="Stop recording" EBRecorder.Close="Close" + +EBRecorder.Hotkey.Start="EB Recorder: Start recording" +EBRecorder.Hotkey.Stop="EB Recorder: Stop recording" diff --git a/data/locale/ru-RU.ini b/data/locale/ru-RU.ini index 87b2422..162698a 100644 --- a/data/locale/ru-RU.ini +++ b/data/locale/ru-RU.ini @@ -1,6 +1,6 @@ EBRecorder.ToolsMenu="EB Recorder" -EBRecorder.Title="EB Recorder 0.3.1" -EBRecorder.Intro="Версия 0.3.1 записывает активный поток Enhanced Broadcasting с максимальным разрешением в устойчивый к аварийному завершению Matroska (MKV), повторно используя уже работающий EB-видеокодировщик и выбранные аудиодорожки Live/VOD. Запись можно запускать вручную или автоматически вместе с EB-трансляцией." +EBRecorder.Title="EB Recorder 0.3.2" +EBRecorder.Intro="Версия 0.3.2 записывает активный поток Enhanced Broadcasting с максимальным разрешением в устойчивый к аварийному завершению Matroska (MKV), повторно используя уже работающий EB-видеокодировщик и выбранные аудиодорожки Live/VOD. Запись можно запускать вручную или автоматически вместе с EB-трансляцией." EBRecorder.Status.None="Активные видеокодировщики Enhanced Broadcasting не найдены. Запусти EB-трансляцию и обнови список." EBRecorder.Status.Found="Обнаружено активных видеокодировщиков Enhanced Broadcasting: %1." EBRecorder.Selected.None="Верхний EB-поток: недоступен." @@ -47,3 +47,6 @@ EBRecorder.Refresh="Обновить" EBRecorder.StartRecording="Начать запись" EBRecorder.StopRecording="Остановить запись" EBRecorder.Close="Закрыть" + +EBRecorder.Hotkey.Start="EB Recorder: Начать запись" +EBRecorder.Hotkey.Stop="EB Recorder: Остановить запись" diff --git a/release-notes/README.md b/release-notes/README.md index 5595a5b..7c4fb7a 100644 --- a/release-notes/README.md +++ b/release-notes/README.md @@ -2,6 +2,7 @@ Detailed release notes for EB Recorder are stored here, one Markdown file per published version. +- [v0.3.2](v0.3.2.md) — native OBS start/stop hotkeys - [v0.3.1](v0.3.1.md) — Live/VOD audio selection and bitrate metadata - [v0.3.0](v0.3.0.md) — first public release diff --git a/release-notes/v0.3.2.md b/release-notes/v0.3.2.md new file mode 100644 index 0000000..3ac6e35 --- /dev/null +++ b/release-notes/v0.3.2.md @@ -0,0 +1,30 @@ +# EB Recorder v0.3.2 + +EB Recorder v0.3.2 adds native OBS hotkeys for starting and stopping EB Recorder without opening the plugin window. + +## Highlights + +- Adds **EB Recorder: Start recording** to OBS **Settings → Hotkeys**. +- Adds **EB Recorder: Stop recording** to OBS **Settings → Hotkeys**. +- Hotkeys use OBS's native frontend hotkey system; no global keyboard hook is added by the plugin. +- Hotkey bindings are stored by OBS in the current profile and reloaded when the OBS profile changes. +- Start hotkey uses the same TOP EB video and Live/VOD audio-selection logic as the plugin window. +- Stop hotkey performs the normal clean stop, including Matroska `BPS` audio-bitrate metadata finalization. +- Repeated Start while already recording and Stop while idle are ignored safely. +- The zero-extra-encoder architecture remains unchanged. + +## Tested target + +- OBS Studio 32.2.2 x64 +- Windows 10/11 x64 +- Twitch Enhanced Broadcasting / Multitrack Video + +## Installation + +1. Close OBS. +2. Extract `EBRecorder-v0.3.2-Windows-x64.zip`. +3. Run `INSTALL.cmd`. +4. Start OBS. +5. Open **Settings → Hotkeys** and assign keys to the two EB Recorder actions. + +This version updates existing 0.3.1 installations in place. diff --git a/scripts/build-windows.ps1 b/scripts/build-windows.ps1 index 646960e..106b2ae 100644 --- a/scripts/build-windows.ps1 +++ b/scripts/build-windows.ps1 @@ -244,7 +244,7 @@ Write-Host "Visual Studio path: $($EnvInfo.VSPath)" Write-Host "CMake: $($EnvInfo.CMakeVersionLine)" Write-Host "CMake path: $CMakeExe" Write-Host '' -Write-Host 'EB Recorder r6.3 production build strategy:' +Write-Host 'EB Recorder r6.4 production build strategy:' Write-Host ' - no obs-plugintemplate bootstrap' Write-Host ' - one OBS x64 SDK build only' Write-Host ' - no nested Win32 build' @@ -322,7 +322,7 @@ if (-not (Test-Path $ObsDepsPrefix)) { $PrefixPath = "$ObsSdk;$ObsDepsPrefix;$QtPrefix" Write-Host '' -Write-Host 'EB Recorder build package: r6.3 (production Release)' -ForegroundColor Cyan +Write-Host 'EB Recorder build package: r6.4 (production Release)' -ForegroundColor Cyan Write-Host 'Configuring EB Recorder plugin...' $PluginConfigureArgs = @( '-S', $Root, diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 877b1c2..6175f60 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -1,7 +1,7 @@ $ErrorActionPreference = 'Stop' -$PluginVersion = '0.3.1' -$BuildRevision = 'r6.3' +$PluginVersion = '0.3.2' +$BuildRevision = 'r6.4' if (Get-Process obs64 -ErrorAction SilentlyContinue) { Write-Host 'OBS Studio is running. Close OBS and run the installer again.' -ForegroundColor Yellow diff --git a/src/plugin-main.cpp b/src/plugin-main.cpp index b8a6178..d18b917 100644 --- a/src/plugin-main.cpp +++ b/src/plugin-main.cpp @@ -46,12 +46,14 @@ OBS_DECLARE_MODULE() OBS_MODULE_USE_DEFAULT_LOCALE("eb-recorder", "en-US") namespace { -constexpr const char *kVersion = "0.3.1"; +constexpr const char *kVersion = "0.3.2"; constexpr const char *kEbEncoderPrefix = "multitrack video video encoder "; constexpr const char *kEbLiveAudioEncoderName = "multitrack video live audio 0"; constexpr const char *kEbVodAudioEncoderName = "multitrack video vod audio 0"; constexpr const char *kLocalOutputName = "eb-recorder local output"; constexpr const char *kLocalOutputType = "ffmpeg_muxer"; +constexpr const char *kStartHotkeyName = "EBRecorder.StartRecording"; +constexpr const char *kStopHotkeyName = "EBRecorder.StopRecording"; // Keep Matroska clusters short so an interrupted session leaves only a small // tail at risk. The MKV container itself does not depend on a final MOOV-like // index to remain readable after an abrupt process/power loss. @@ -109,6 +111,7 @@ bool g_cleanStopRequested = false; bool g_autoStartPending = false; qint64 g_autoStartDeadlineMs = 0; QTimer *g_autoStartTimer = nullptr; +obs_hotkey_pair_id g_recordingHotkeys = OBS_INVALID_HOTKEY_PAIR_ID; QString fallbackText(const char *key) { @@ -122,10 +125,10 @@ QString fallbackText(const char *key) }; static const Translation translations[] = { - {"EBRecorder.Title", "EB Recorder 0.3.1", "EB Recorder 0.3.1"}, + {"EBRecorder.Title", "EB Recorder 0.3.2", "EB Recorder 0.3.2"}, {"EBRecorder.Intro", - "Version 0.3.1 records the active highest-resolution Enhanced Broadcasting rendition to crash-resilient Matroska (MKV) by reusing the existing EB video and selected Live/VOD audio encoders. Recording can be started manually or automatically together with the EB stream.", - "Версия 0.3.1 записывает активный поток Enhanced Broadcasting с максимальным разрешением в устойчивый к аварийному завершению Matroska (MKV), повторно используя уже работающий EB-видеокодировщик и выбранные аудиодорожки Live/VOD. Запись можно запускать вручную или автоматически вместе с EB-трансляцией."}, + "Version 0.3.2 records the active highest-resolution Enhanced Broadcasting rendition to crash-resilient Matroska (MKV) by reusing the existing EB video and selected Live/VOD audio encoders. Recording can be started manually or automatically together with the EB stream.", + "Версия 0.3.2 записывает активный поток Enhanced Broadcasting с максимальным разрешением в устойчивый к аварийному завершению Matroska (MKV), повторно используя уже работающий EB-видеокодировщик и выбранные аудиодорожки Live/VOD. Запись можно запускать вручную или автоматически вместе с EB-трансляцией."}, {"EBRecorder.Status.None", "No active Enhanced Broadcasting video encoders detected. Start an EB stream and refresh.", "Активные видеокодировщики Enhanced Broadcasting не найдены. Запусти EB-трансляцию и обнови список."}, @@ -193,6 +196,8 @@ QString fallbackText(const char *key) {"EBRecorder.StartRecording", "Start recording", "Начать запись"}, {"EBRecorder.StopRecording", "Stop recording", "Остановить запись"}, {"EBRecorder.Close", "Close", "Закрыть"}, + {"EBRecorder.Hotkey.Start", "EB Recorder: Start recording", "EB Recorder: Начать запись"}, + {"EBRecorder.Hotkey.Stop", "EB Recorder: Stop recording", "EB Recorder: Остановить запись"}, }; for (const auto &entry : translations) { @@ -1457,6 +1462,130 @@ void setAudioSelection(AudioSelection selection) armAutomaticRecording("audio selection changed during active stream"); } +bool startRecordingFromCurrentEb(QString &error, const char *reason) +{ + releaseInactiveRecordingOutput(); + if (recordingOutputActive()) { + error = ebTr("EBRecorder.Error.AlreadyActive"); + return false; + } + + const EbAudioState audio = getEbAudioState(); + const VodTrackConfigState vodConfig = getObsVodTrackConfigState(); + normalizeAudioSelectionForVodAvailability(audio, vodConfig, reason); + + const auto encoders = getEbEncoders(); + const EncoderInfo *top = findTopEncoder(encoders); + if (!top) { + error = ebTr("EBRecorder.Error.NoTop"); + return false; + } + if (!selectedAudioReady(audio)) { + error = ebTr("EBRecorder.Error.NoAudio").arg(audioSelectionLabel(g_audioSelection)); + return false; + } + + blog(LOG_INFO, "[EB Recorder] recording start requested (%s)", reason ? reason : "unspecified"); + return startLocalRecording(*top, error); +} + +obs_data_array_t *loadFrontendHotkeyBindings(config_t *config, const char *name) +{ + obs_data_array_t *bindings = obs_data_array_create(); + if (!config || !name) + return bindings; + + const char *json = config_get_string(config, "Hotkeys", name); + if (!json || !*json) + return bindings; + + obs_data_t *data = obs_data_create_from_json(json); + if (!data) + return bindings; + + obs_data_array_t *saved = obs_data_get_array(data, "bindings"); + if (saved) { + obs_data_array_release(bindings); + bindings = saved; + } + obs_data_release(data); + return bindings; +} + +void loadRecordingHotkeyBindings() +{ + if (g_recordingHotkeys == OBS_INVALID_HOTKEY_PAIR_ID) + return; + + config_t *config = obs_frontend_get_profile_config(); + obs_data_array_t *startBindings = loadFrontendHotkeyBindings(config, kStartHotkeyName); + obs_data_array_t *stopBindings = loadFrontendHotkeyBindings(config, kStopHotkeyName); + obs_hotkey_pair_load(g_recordingHotkeys, startBindings, stopBindings); + obs_data_array_release(startBindings); + obs_data_array_release(stopBindings); + blog(LOG_INFO, "[EB Recorder] recording hotkey bindings loaded from current OBS profile"); +} + +bool startRecordingHotkey(void *, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) +{ + if (!pressed || recordingOutputActive()) + return false; + + QString error; + if (!startRecordingFromCurrentEb(error, "hotkey")) { + blog(LOG_INFO, "[EB Recorder] start hotkey ignored: %s", error.toUtf8().constData()); + refreshDialogIfOpen(); + return false; + } + + cancelPendingAutoStart("recording started by hotkey"); + blog(LOG_INFO, "[EB Recorder] recording started by hotkey"); + refreshDialogIfOpen(); + return true; +} + +bool stopRecordingHotkey(void *, obs_hotkey_pair_id, obs_hotkey_t *, bool pressed) +{ + if (!pressed || !recordingOutputActive()) + return false; + + cancelPendingAutoStart("recording stopped by hotkey"); + stopLocalRecording(false, "hotkey"); + blog(LOG_INFO, "[EB Recorder] recording stop requested by hotkey"); + refreshDialogIfOpen(); + return true; +} + +void registerRecordingHotkeys() +{ + if (g_recordingHotkeys != OBS_INVALID_HOTKEY_PAIR_ID) + return; + + const QByteArray startDescription = ebTr("EBRecorder.Hotkey.Start").toUtf8(); + const QByteArray stopDescription = ebTr("EBRecorder.Hotkey.Stop").toUtf8(); + g_recordingHotkeys = obs_hotkey_pair_register_frontend( + kStartHotkeyName, startDescription.constData(), kStopHotkeyName, stopDescription.constData(), + startRecordingHotkey, stopRecordingHotkey, nullptr, nullptr); + + if (g_recordingHotkeys == OBS_INVALID_HOTKEY_PAIR_ID) { + blog(LOG_WARNING, "[EB Recorder] failed to register recording hotkeys"); + return; + } + + loadRecordingHotkeyBindings(); + blog(LOG_INFO, "[EB Recorder] recording hotkeys registered"); +} + +void unregisterRecordingHotkeys() +{ + if (g_recordingHotkeys == OBS_INVALID_HOTKEY_PAIR_ID) + return; + + obs_hotkey_pair_unregister(g_recordingHotkeys); + g_recordingHotkeys = OBS_INVALID_HOTKEY_PAIR_ID; + blog(LOG_INFO, "[EB Recorder] recording hotkeys unregistered"); +} + class EBRecorderDialog final : public QDialog { public: explicit EBRecorderDialog(QWidget *parent) : QDialog(parent) @@ -1598,24 +1727,11 @@ class EBRecorderDialog final : public QDialog { void startRecording() { - const EbAudioState audio = getEbAudioState(); - const VodTrackConfigState vodConfig = getObsVodTrackConfigState(); - if (normalizeAudioSelectionForVodAvailability(audio, vodConfig, "manual start")) { - QSignalBlocker blocker(audioSelectionCombo_); - audioSelectionCombo_->setCurrentIndex(static_cast(g_audioSelection)); - } - - const auto encoders = getEbEncoders(); - const EncoderInfo *top = findTopEncoder(encoders); - if (!top) { - setRecordingError(ebTr("EBRecorder.Error.NoTop")); - refreshEncoders(true); - return; - } - QString error; - if (!startLocalRecording(*top, error)) + if (!startRecordingFromCurrentEb(error, "dialog button")) setRecordingError(error); + else + cancelPendingAutoStart("recording started from dialog"); refreshEncoders(true); } @@ -1669,7 +1785,8 @@ class EBRecorderDialog final : public QDialog { const EbAudioState audio = getEbAudioState(); const VodTrackConfigState vodConfig = getObsVodTrackConfigState(); const bool vodSelectable = vodSelectionAllowed(audio, vodConfig); - if (normalizeAudioSelectionForVodAvailability(audio, vodConfig, "dialog refresh")) { + normalizeAudioSelectionForVodAvailability(audio, vodConfig, "dialog refresh"); + if (audioSelectionCombo_->currentIndex() != static_cast(g_audioSelection)) { QSignalBlocker blocker(audioSelectionCombo_); audioSelectionCombo_->setCurrentIndex(static_cast(g_audioSelection)); } @@ -1850,6 +1967,9 @@ void frontendEvent(enum obs_frontend_event event, void *) if (g_autoRecordEnabled) armAutomaticRecording("OBS streaming started"); break; + case OBS_FRONTEND_EVENT_PROFILE_CHANGED: + loadRecordingHotkeyBindings(); + break; default: return; } @@ -1897,6 +2017,7 @@ bool obs_module_load(void) logLocaleDiagnostics(); initializeSettingsPath(); g_obsExiting = false; + registerRecordingHotkeys(); obs_frontend_add_tools_menu_item("EB Recorder", openDialog, nullptr); obs_frontend_add_event_callback(frontendEvent, nullptr); g_frontendCallbackRegistered = true; @@ -1927,6 +2048,7 @@ void obs_module_unload(void) g_dialog.isNull() ? "null" : "alive", recordingOutputActive() ? "active" : "inactive"); cancelPendingAutoStart("module unload"); + unregisterRecordingHotkeys(); if (g_autoStartTimer) { delete g_autoStartTimer; g_autoStartTimer = nullptr;