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 .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Describe the change and why it is needed.

## Validation

- [ ] `dotnet build .\InstantReplay.sln -c Release`
- [ ] `dotnet build .\Captail.sln -c Release`
- [ ] Real Windows UI checked when applicable
- [ ] Existing replay, hotkey, tray, and recovery behavior preserved
- [ ] New user-facing text added to both EN and RU resources
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
with:
dotnet-version: 9.0.x
cache: true
cache-dependency-path: src/InstantReplay/InstantReplay.csproj
cache-dependency-path: src/Captail/Captail.csproj

- name: Cache OBS runtime
uses: actions/cache@v6
Expand All @@ -43,13 +43,13 @@ jobs:

- name: Build Release
shell: pwsh
run: dotnet build ./InstantReplay.sln -c Release -p:ContinuousIntegrationBuild=true
run: dotnet build ./Captail.sln -c Release -p:ContinuousIntegrationBuild=true

- name: Validate localization dictionaries
shell: pwsh
run: |
[xml]$en = Get-Content ./src/InstantReplay/Languages/Strings.en.xaml
[xml]$ru = Get-Content ./src/InstantReplay/Languages/Strings.ru.xaml
[xml]$en = Get-Content ./src/Captail/Languages/Strings.en.xaml
[xml]$ru = Get-Content ./src/Captail/Languages/Strings.ru.xaml
$enKeys = @($en.ResourceDictionary.String | ForEach-Object { $_.Key })
$ruKeys = @($ru.ResourceDictionary.String | ForEach-Object { $_.Key })
$differences = @(Compare-Object $enKeys $ruKeys)
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
with:
dotnet-version: 9.0.x
cache: true
cache-dependency-path: src/InstantReplay/InstantReplay.csproj
cache-dependency-path: src/Captail/Captail.csproj

- name: Cache OBS runtime
uses: actions/cache@v6
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

All notable user-facing changes are documented here.

## [Unreleased]

### Changed

- Renamed remaining solution, project, namespace, icon, and native bridge identifiers to Captail.
- Added a real interface screenshot and an OBS Replay Buffer comparison to the README.
- Standardized repository comments and diagnostic messages in English.

## [0.1.0] - 2026-07-20

First public preview.
Expand Down
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Requirements:

```powershell
.\tools\AcquireObsRuntime.ps1
dotnet build .\InstantReplay.sln -c Debug
dotnet build .\Captail.sln -c Debug
```

## Pull requests
Expand All @@ -43,6 +43,8 @@ dotnet build .\InstantReplay.sln -c Debug
## Code style

- Follow existing C# and XAML conventions.
- Use English for GitHub issues, pull requests, documentation, code comments, and diagnostic messages.
- Keep Russian text only in the Russian localization dictionary.
- Prefer clear behavior over abstraction.
- Keep user-facing text in EN/RU localization dictionaries.
- Do not present unavailable hardware functionality as enabled.
Expand Down
2 changes: 1 addition & 1 deletion InstantReplay.sln → Captail.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InstantReplay", "src\InstantReplay\InstantReplay.csproj", "{FB66E8EA-3BE5-4E9C-8533-1C1B10A06151}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Captail", "src\Captail\Captail.csproj", "{FB66E8EA-3BE5-4E9C-8533-1C1B10A06151}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@

Captail is designed around one rule: **Instant Replay should stay on.** A game crash, game-to-desktop switch, temporary capture failure, protected surface, or recoverable graphics-driver interruption should not silently leave you without a replay. A watchdog monitors the recording pipeline and restarts it when recovery is possible.

<p align="center">
<img src="docs/captail-main.jpg" alt="Captail main window showing replay status, audio sources, recording format, disk space, and save hotkey" width="420">
</p>

> [!WARNING]
> Captail `v0.1.x` is an early public preview. Core recording works, but bugs and hardware-specific problems are expected. Please report anything that does not work.

Expand All @@ -27,6 +31,16 @@ Captail is designed around one rule: **Instant Replay should stay on.** A game c
- **Flexible audio** — system/game audio and microphone, volume controls, microphone boost, mixed or separate tracks.
- **Local and private** — no account, cloud upload, analytics, or telemetry. Replays stay on your PC.

## How Captail differs from OBS Replay Buffer

Captail uses libobs for capture and encoding, but it is not an OBS Studio frontend.

- Starts directly in the system tray with Windows.
- Requires no scenes, sources, or streaming configuration.
- Monitors capture health and automatically recovers the recording pipeline.
- Switches between focused game capture and desktop capture.
- Stays focused on one job: reliable instant replay.

## Download

Download the latest version from [GitHub Releases](https://github.com/FaulMit/captail/releases).
Expand Down Expand Up @@ -147,7 +161,7 @@ Requirements:
git clone https://github.com/FaulMit/captail.git
cd captail
.\tools\AcquireObsRuntime.ps1
dotnet build .\InstantReplay.sln -c Release
dotnet build .\Captail.sln -c Release
```

Create local release packages:
Expand Down
Binary file added docs/captail-main.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion installer/Captail.iss
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ ArchitecturesInstallIn64BitMode=x64compatible
MinVersion=10.0.19041
OutputDir={#OutputDir}
OutputBaseFilename=Captail-{#MyAppVersion}-Setup-win-x64
SetupIconFile=..\src\InstantReplay\Assets\Everloop.ico
SetupIconFile=..\src\Captail\Assets\Captail.ico
UninstallDisplayIcon={app}\{#MyAppExeName}
UninstallDisplayName={#MyAppName}
Uninstallable=yes
Expand Down
8 changes: 4 additions & 4 deletions native/ObsBridge/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.20)
project(EverloopObsBridge LANGUAGES CXX)
project(CaptailObsBridge LANGUAGES CXX)

add_library(EverloopObsBridge SHARED EverloopObsBridge.cpp)
target_compile_features(EverloopObsBridge PRIVATE cxx_std_20)
target_compile_definitions(EverloopObsBridge PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX)
add_library(CaptailObsBridge SHARED CaptailObsBridge.cpp)
target_compile_features(CaptailObsBridge PRIVATE cxx_std_20)
target_compile_definitions(CaptailObsBridge PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX)
4 changes: 2 additions & 2 deletions native/ObsCaptureFixture/ObsCaptureFixture.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int show)
WNDCLASSW window_class{};
window_class.hInstance = instance;
window_class.lpfnWndProc = window_proc;
window_class.lpszClassName = L"EverloopObsCaptureFixture";
window_class.lpszClassName = L"CaptailObsCaptureFixture";
window_class.hCursor = LoadCursorW(nullptr, MAKEINTRESOURCEW(32512));
if (!RegisterClassW(&window_class))
return 1;

HWND window = CreateWindowExW(
0,
window_class.lpszClassName,
L"Everloop OBS Capture Fixture",
L"Captail OBS Capture Fixture",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
Expand Down
2 changes: 1 addition & 1 deletion src/InstantReplay/App.xaml → src/Captail/App.xaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Application x:Class="InstantReplay.App"
<Application x:Class="Captail.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
ShutdownMode="OnExplicitShutdown">
Expand Down
32 changes: 16 additions & 16 deletions src/InstantReplay/App.xaml.cs → src/Captail/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
using System.Windows.Threading;
using H.NotifyIcon;

namespace InstantReplay;
namespace Captail;

public partial class App : Application
{
Expand Down Expand Up @@ -355,7 +355,7 @@ private async void RunGameCaptureTest(string[] args)
OutputDirectory = root,
};
if (!TryStartPipeline(showError: false))
throw new InvalidOperationException("OBS Game Capture не запустился.");
throw new InvalidOperationException("OBS Game Capture did not start.");

await Task.Delay(TimeSpan.FromSeconds(8));
string path = await _obs!.SaveReplayAsync();
Expand Down Expand Up @@ -397,11 +397,11 @@ private async void RunFaultRecoveryTest()
};
StartHealthMonitor();
if (!TryStartPipeline(showError: false))
throw new InvalidOperationException("Исходный OBS pipeline не запустился.");
throw new InvalidOperationException("The initial OBS pipeline did not start.");

DateTime originalStart = _pipelineStartedUtc;
await Task.Delay(TimeSpan.FromSeconds(3));
RecoverPipeline("QA: искусственный перезапуск OBS.");
RecoverPipeline("QA: simulated OBS restart.");
await Task.Delay(TimeSpan.FromSeconds(6));
bool restarted = IsReplayRunning && _pipelineStartedUtc > originalStart;
string path = restarted
Expand Down Expand Up @@ -471,8 +471,8 @@ private bool AcquireSingleInstance(bool backgroundLaunch, bool isolatedUiTest)
Environment.UserName;
string suffix = userId.Replace('\\', '.') +
(isolatedUiTest ? ".UiOnly" : "");
string mutexName = $@"Local\Everloop.SingleInstance.{suffix}";
_activationPipeName = $"Everloop.Activate.{suffix}";
string mutexName = $@"Local\Captail.SingleInstance.{suffix}";
_activationPipeName = $"Captail.Activate.{suffix}";
_singleInstanceMutex = new Mutex(
initiallyOwned: true,
mutexName,
Expand Down Expand Up @@ -503,11 +503,11 @@ private void SendActivationCommand(string command)
}
catch (TimeoutException)
{
// Первый экземпляр ещё запускается.
// The first instance may still be starting.
}
catch (IOException)
{
// Pipe между попытками пересоздаётся.
// The pipe is recreated between attempts.
}
}
}
Expand Down Expand Up @@ -550,7 +550,7 @@ private async Task ActivationServerLoopAsync(CancellationToken cancellationToken
private static void TerminateLegacyInstances()
{
int currentId = Environment.ProcessId;
foreach (Process process in Process.GetProcessesByName("InstantReplay"))
foreach (Process process in Process.GetProcessesByName("Captail"))
{
using (process)
{
Expand All @@ -568,7 +568,7 @@ private static void TerminateLegacyInstances()
catch (Exception exception)
{
Log.Write(
$"Не удалось закрыть старый экземпляр PID {process.Id}: " +
$"Failed to close the previous instance PID {process.Id}: " +
exception.Message);
}
}
Expand All @@ -588,7 +588,7 @@ private void BindHotkeyAtStartup()
}
catch (Exception exception)
{
Log.Write($"Глобальный хоткей недоступен: {exception.Message}");
Log.Write($"Global hotkey unavailable: {exception.Message}");
_pendingUiError = exception.Message;
ShowOverlayNotification(
"!",
Expand Down Expand Up @@ -639,7 +639,7 @@ private bool TryStartPipeline(bool showError)
if (engine is not null)
_capabilities = engine.Capabilities;
StopPipeline();
Log.Write($"Запуск OBS pipeline не удался: {exception}");
Log.Write($"OBS pipeline startup failed: {exception}");
if (showError)
{
ShowOverlayNotification(
Expand Down Expand Up @@ -667,7 +667,7 @@ private void StopPipeline()
}
catch (Exception exception)
{
Log.Write($"Остановка OBS pipeline: {exception}");
Log.Write($"OBS pipeline shutdown failed: {exception}");
}
}

Expand Down Expand Up @@ -1119,7 +1119,7 @@ private async Task SaveReplayCoreAsync(ObsReplayEngine engine)
}
catch (Exception exception)
{
Log.Write($"Сохранение повтора упало: {exception}");
Log.Write($"Replay save failed: {exception}");
ShowOverlayNotification(
"!",
Localization.Text("L.Notify.SaveError"),
Expand Down Expand Up @@ -1161,7 +1161,7 @@ private static string FormatDuration(int seconds) =>
private static Icon CreateIcon()
{
using Stream stream = GetResourceStream(
new Uri("Assets/Everloop.ico", UriKind.Relative)).Stream;
new Uri("Assets/Captail.ico", UriKind.Relative)).Stream;
using var icon = new Icon(stream);
return (Icon)icon.Clone();
}
Expand All @@ -1185,7 +1185,7 @@ protected override void OnExit(ExitEventArgs e)
}
catch
{
// Уже освобождён во время аварийного завершения.
// Already released during emergency shutdown.
}
_singleInstanceMutex.Dispose();
}
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using NAudio.CoreAudioApi;

namespace InstantReplay;
namespace Captail;

internal static class AudioDevices
{
Expand Down
9 changes: 3 additions & 6 deletions src/InstantReplay/Autostart.cs → src/Captail/Autostart.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
using Microsoft.Win32;

namespace InstantReplay;
namespace Captail;

/// <summary>Автозапуск через HKCU\...\Run — без прав администратора.</summary>
/// <summary>Per-user startup through HKCU\...\Run; administrator rights are not required.</summary>
public static class Autostart
{
private const string RunKey = @"Software\Microsoft\Windows\CurrentVersion\Run";
private const string ValueName = "Captail";
private const string LegacyValueName = "InstantReplay";

public static bool IsEnabled()
{
using var key = Registry.CurrentUser.OpenSubKey(RunKey);
return HasCommand(key, ValueName) || HasCommand(key, LegacyValueName);
return HasCommand(key, ValueName);
}

public static void SetEnabled(bool enabled)
Expand All @@ -27,12 +26,10 @@ public static void SetEnabled(bool enabled)
ValueName,
$"\"{executablePath}\" --background",
RegistryValueKind.String);
key.DeleteValue(LegacyValueName, throwOnMissingValue: false);
}
else
{
key.DeleteValue(ValueName, throwOnMissingValue: false);
key.DeleteValue(LegacyValueName, throwOnMissingValue: false);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
<AssemblyName>Captail</AssemblyName>
<AssemblyTitle>Captail</AssemblyTitle>
<Product>Captail</Product>
<ApplicationIcon>Assets\Everloop.ico</ApplicationIcon>
<RootNamespace>Captail</RootNamespace>
<ApplicationIcon>Assets\Captail.ico</ApplicationIcon>
<Version>0.1.0</Version>
</PropertyGroup>

Expand All @@ -23,7 +24,7 @@
</ItemGroup>

<ItemGroup>
<Resource Include="Assets\Everloop.ico" />
<Resource Include="Assets\Captail.ico" />
<Content Include="..\..\LICENSE" Link="LICENSE">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
Expand Down Expand Up @@ -51,21 +52,21 @@
</Target>

<Target Name="BuildObsBridge" BeforeTargets="Build">
<Exec Condition="!Exists('$(ObsBridgeBuild)\EverloopObsBridge.sln')"
<Exec Condition="!Exists('$(ObsBridgeBuild)\CaptailObsBridge.sln')"
Command="cmake -S &quot;$(ObsBridgeSource)&quot; -B &quot;$(ObsBridgeBuild)&quot; -G &quot;Visual Studio 17 2022&quot; -A x64" />
<Exec Command="cmake --build &quot;$(ObsBridgeBuild)&quot; --config Release --target EverloopObsBridge" />
<Exec Command="cmake --build &quot;$(ObsBridgeBuild)&quot; --config Release --target CaptailObsBridge" />
</Target>

<Target Name="CopyObsBridge" AfterTargets="Build" DependsOnTargets="BuildObsBridge">
<Copy SourceFiles="$(ObsBridgeBuild)\Release\EverloopObsBridge.dll"
<Copy SourceFiles="$(ObsBridgeBuild)\Release\CaptailObsBridge.dll"
DestinationFolder="$(OutDir)" />
</Target>

<Target Name="CopyObsBridgeToPublish"
AfterTargets="Publish"
DependsOnTargets="BuildObsBridge"
Condition="'$(PublishDir)' != ''">
<Copy SourceFiles="$(ObsBridgeBuild)\Release\EverloopObsBridge.dll"
<Copy SourceFiles="$(ObsBridgeBuild)\Release\CaptailObsBridge.dll"
DestinationFolder="$(PublishDir)" />
</Target>

Expand Down
Loading
Loading