Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion BUILD-FULL-CLEAN.cmd
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions README_RU.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.3.1
0.3.2
7 changes: 5 additions & 2 deletions data/locale/en-US.ini
Original file line number Diff line number Diff line change
@@ -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."
Expand Down Expand Up @@ -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"
7 changes: 5 additions & 2 deletions data/locale/ru-RU.ini
Original file line number Diff line number Diff line change
@@ -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-поток: недоступен."
Expand Down Expand Up @@ -47,3 +47,6 @@ EBRecorder.Refresh="Обновить"
EBRecorder.StartRecording="Начать запись"
EBRecorder.StopRecording="Остановить запись"
EBRecorder.Close="Закрыть"

EBRecorder.Hotkey.Start="EB Recorder: Начать запись"
EBRecorder.Hotkey.Stop="EB Recorder: Остановить запись"
1 change: 1 addition & 0 deletions release-notes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
30 changes: 30 additions & 0 deletions release-notes/v0.3.2.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions scripts/build-windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions scripts/install.ps1
Original file line number Diff line number Diff line change
@@ -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
Expand Down
164 changes: 143 additions & 21 deletions src/plugin-main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
{
Expand All @@ -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-трансляцию и обнови список."},
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<int>(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);
}

Expand Down Expand Up @@ -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<int>(g_audioSelection)) {
QSignalBlocker blocker(audioSelectionCombo_);
audioSelectionCombo_->setCurrentIndex(static_cast<int>(g_audioSelection));
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading