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
35 changes: 26 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,29 @@ Captail is designed around one rule: **Instant Replay should stay on.** A game c
## Interface

<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">
<img src="docs/captail-main.png" alt="Captail main window showing replay status, audio sources, recording format, disk space, and the scrollable replay library" width="420">
</p>

<p align="center">
<img src="docs/captail-settings-video.jpg" alt="Captail video settings with source, codec, bitrate, resolution, and frame-rate controls" width="390">
<img src="docs/captail-settings-audio.jpg" alt="Captail audio, storage, and hotkey settings" width="390">
</p>

<p align="center">
<img src="docs/captail-editor.png" alt="Captail clip editor with video timeline, separate audio tracks, trim range, and live media details" width="820">
</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.

## What's new in v0.1.3
## What's new in v0.1.4

- **Built-in updates** — the footer shows update status and installs the correct Installer or Portable package after verifying its SHA-256 digest.
- **Clearer settings** — concise help popups explain codecs, bitrate, resolution, frame rate, audio tracks, buffer limits, and other technical options in English and Russian.
- **Non-overlapping saves** — after saving a replay, the next clip starts at that save boundary instead of repeating footage from the previous file.
- **More accurate save duration** — the main action shows how much new footage is currently available.
- **Safer recovery** — the watchdog no longer treats normal recording-pipeline startup as a failure.
- **Replay library** — every saved replay is available from the main window in a fast, scrollable list with thumbnail previews.
- **Cleaner replay actions** — hover a clip for compact Open in folder, Trim, and Delete controls. Deletion always requires confirmation.
- **Built-in clip editor** — scrub through video, select one trim range, preview both audio tracks, keep or remove each track, then save a copy or overwrite the original.
- **Useful clip details** — the editor shows estimated trimmed size, current file size, resolution, true source FPS, and codec while the range changes.
- **More reliable preview** — full-frame DPI-aware playback replaces the cropped upper-left preview and includes system/game plus microphone audio.
- **Automatic game capture** — Desktop mode records the selected monitor and switches to direct Game Capture for a detected fullscreen game. Game Capture mode waits for any game without recording the desktop.

## Why Captail?

Expand Down Expand Up @@ -77,7 +82,7 @@ Captail checks GitHub Releases in the background. When an update is available, t

1. Install Captail or extract the Portable ZIP.
2. Launch `Captail.exe`.
3. Select **Desktop** or a running game under **Source**.
3. Select **Desktop** for automatic desktop/game switching, or **Game Capture** to record games only.
4. Choose buffer length, codec, resolution, FPS, and audio sources.
5. Keep Instant Replay enabled.
6. Press `Ctrl+Shift+F10` to save the current buffer.
Expand Down Expand Up @@ -125,6 +130,15 @@ Both hotkeys can be changed in Settings.
- Direct repository link in the footer.
- English interface by default, with live English/Russian switching.

### Replay library and editor

- Scrollable library containing every supported replay in the selected folder.
- Cached thumbnail strip and responsive seeking.
- One visual trim range with draggable start and end handles.
- Independent system/game and microphone track selection.
- Save as a new clip or overwrite the original after confirmation.
- Live estimated output size, source size, resolution, FPS, and codec.

### Reliability

- Recording-pipeline watchdog.
Expand Down Expand Up @@ -187,7 +201,7 @@ dotnet build .\Captail.sln -c Release
Create local release packages:

```powershell
.\tools\BuildRelease.ps1 -Version 0.1.0 -InnoSetupCompiler "C:\Program Files\Inno Setup 7\ISCC.exe"
.\tools\BuildRelease.ps1 -Version 0.1.4 -InnoSetupCompiler "C:\Program Files\Inno Setup 7\ISCC.exe"
```

See [CONTRIBUTING.md](CONTRIBUTING.md) for development guidelines and [docs/RELEASING.md](docs/RELEASING.md) for the automated release process.
Expand All @@ -200,6 +214,9 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development guidelines and [docs/RELE
- `native/ObsCaptureFixture` — D3D11 fixture for Game Capture and high-FPS validation.
- `App.xaml.cs` — tray, hotkeys, notifications, single-instance behavior, watchdog, and recovery.
- `SettingsWindow.*` — compact WPF interface.
- `ReplayLibrary.cs` — replay discovery, metadata, thumbnail cache, delete, and trim orchestration.
- `ClipEditorWindow.*` — preview, timeline, audio-track selection, and copy/overwrite workflows.
- `FfmpegAdapter.cs` and `FfplayHost.cs` — bundled media probing, thumbnails, waveforms, trimming, and editor playback.
- `UpdateService.cs` — GitHub release discovery, package verification, Installer updates, and Portable self-replacement.

## License and attribution
Expand Down
9 changes: 9 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ Captail dynamically uses and may redistribute selected components from OBS Studi

`tools/AcquireObsRuntime.ps1` prepares the runtime. Distributions contain only components required for libobs, Replay Buffer, Windows capture, WASAPI audio, and supported hardware encoders.

## FFmpeg

- Project: https://ffmpeg.org/
- Windows build: https://github.com/BtbN/FFmpeg-Builds
- Build: `n7.1.5-12-g1fdbca85aa`, LGPL shared variant
- License: GNU Lesser General Public License 2.1 or later; optional components retain their own licenses

`tools/AcquireFfmpegRuntime.ps1` downloads a pinned, SHA-256-verified build. Captail uses it for clip metadata, thumbnails, and non-destructive trimming.

## NuGet dependencies

- NAudio — MIT License: https://github.com/naudio/NAudio
Expand Down
Binary file added docs/captail-editor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/captail-main.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
137 changes: 122 additions & 15 deletions src/Captail/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace Captail;

public partial class App : Application

Check warning on line 14 in src/Captail/App.xaml.cs

View workflow job for this annotation

GitHub Actions / Windows x64

Because an application's API isn't typically referenced from outside the assembly, types can be made internal (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1515)
{
private Config? _config;
private ObsReplayEngine? _obs;
Expand Down Expand Up @@ -80,6 +80,19 @@
bool updateCheckTest = e.Args.Contains(
"--qa-update-check",
StringComparer.OrdinalIgnoreCase);
string? clipEditorTestPath = e.Args
.FirstOrDefault(argument => argument.StartsWith(
"--qa-clip-editor=",
StringComparison.OrdinalIgnoreCase))
?["--qa-clip-editor=".Length..];
bool clipEditorTest = !string.IsNullOrWhiteSpace(clipEditorTestPath);
string? previewGeometryTestPath = e.Args
.FirstOrDefault(argument => argument.StartsWith(
"--qa-preview-geometry=",
StringComparison.OrdinalIgnoreCase))
?["--qa-preview-geometry=".Length..];
bool previewGeometryTest =
!string.IsNullOrWhiteSpace(previewGeometryTestPath);
_qaUpdateAvailable = e.Args.Contains(
"--qa-update-available",
StringComparer.OrdinalIgnoreCase);
Expand All @@ -90,6 +103,8 @@
const bool gameCaptureTest = false;
const bool replaySegmentsTest = false;
const bool updateCheckTest = false;
const bool clipEditorTest = false;
const bool previewGeometryTest = false;
#endif
bool backgroundLaunch = e.Args.Contains(
"--background",
Expand All @@ -101,7 +116,8 @@
backgroundLaunch,
shutdownExisting,
_uiOnly || faultTest || codecTest || capabilityModelTest ||
gameCaptureTest || replaySegmentsTest || updateCheckTest))
gameCaptureTest || replaySegmentsTest || updateCheckTest ||
clipEditorTest || previewGeometryTest))
{
Shutdown();
return;
Expand Down Expand Up @@ -163,6 +179,16 @@
Shutdown(0);
return;
}
if (clipEditorTest)
{
await RunClipEditorTestAsync(clipEditorTestPath!);
return;
}
if (previewGeometryTest)
{
await RunPreviewGeometryTestAsync(previewGeometryTestPath!);
return;
}
#endif
if (_uiOnly)
{
Expand Down Expand Up @@ -224,6 +250,74 @@
}

#if DEBUG
private async Task RunClipEditorTestAsync(string path)
{
string fullPath = Path.GetFullPath(path);
if (!File.Exists(fullPath))
throw new FileNotFoundException("QA replay does not exist.", fullPath);

var ffmpeg = new FfmpegAdapter();
TimeSpan duration = await ffmpeg.ReadDurationAsync(fullPath);
var file = new FileInfo(fullPath);
var clip = new ReplayClip(
fullPath,
file.Name,
null,
file.LastWriteTime,
file.Length,
duration,
null);
var window = new ClipEditorWindow(
new ReplayLibrary(ffmpeg),
file.DirectoryName!,
clip,
saved => Log.Write($"CLIP_EDITOR_QA saved={saved}"));
MainWindow = window;
window.ShowDialog();
Shutdown(0);
}

private async Task RunPreviewGeometryTestAsync(string path)
{
string fullPath = Path.GetFullPath(path);
if (!File.Exists(fullPath))
throw new FileNotFoundException("QA replay does not exist.", fullPath);

var ffmpeg = new FfmpegAdapter();
TimeSpan duration = await ffmpeg.ReadDurationAsync(fullPath);
var file = new FileInfo(fullPath);
var clip = new ReplayClip(
fullPath,
file.Name,
null,
file.LastWriteTime,
file.Length,
duration,
null);
var window = new ClipEditorWindow(
new ReplayLibrary(ffmpeg),
file.DirectoryName!,
clip,
_ => { });
MainWindow = window;
window.Show();
try
{
(bool passed, string details) =
await window.RunPreviewGeometryQaAsync();
Log.Write(
$"PREVIEW_GEOMETRY_TEST {(passed ? "PASS" : "FAIL")}: {details}");
window.Close();
Shutdown(passed ? 0 : 15);
}
catch (Exception exception)
{
Log.Write($"PREVIEW_GEOMETRY_TEST FAIL: {exception}");
window.Close();
Shutdown(15);
}
}

private void RunCapabilityModelTest()
{
try
Expand Down Expand Up @@ -363,7 +457,12 @@
continue;
}

await Task.Delay(TimeSpan.FromSeconds(6));
await Task.Delay(TimeSpan.FromSeconds(2));
bool sourceChanged = _obs!.RefreshCaptureState();
Log.Write(
$"OBS_CODEC_TEST {codec}: source={_obs.Description}, " +
$"changed={sourceChanged}, game={_obs.ActiveGameExecutable}");
await Task.Delay(TimeSpan.FromSeconds(4));
string path = await _obs!.SaveReplayAsync();
bool saved = File.Exists(path) && new FileInfo(path).Length > 0;
allPassed &= saved;
Expand All @@ -387,11 +486,6 @@
{
try
{
string gamePath = args
.First(argument => argument.StartsWith(
"--qa-game-capture=",
StringComparison.OrdinalIgnoreCase))
["--qa-game-capture=".Length..];
string codec = args
.FirstOrDefault(argument => argument.StartsWith(
"--qa-game-codec=",
Expand All @@ -413,7 +507,6 @@
BitrateMbps = 50,
Codec = codec,
CaptureSource = "game",
GameExecutablePath = gamePath,
CaptureSystemAudio = false,
CaptureMicrophone = false,
OutputDirectory = root,
Expand Down Expand Up @@ -958,16 +1051,18 @@
recoveryReason = Localization.Text(
"L.Recovery.ModuleStopped");
}
else if (DateTime.UtcNow - _pipelineStartedUtc <
TimeSpan.FromSeconds(8))
{
return;
}
else
{
bool checkHealth = DateTime.UtcNow - _pipelineStartedUtc >=
TimeSpan.FromSeconds(8);
(bool healthy, string? description) =
await RunOnObsThreadAsync(() =>
(engine.IsHealthy, engine.Description));
{
engine.RefreshCaptureState();
return (
checkHealth ? engine.IsHealthy : true,
engine.Description);
});
if (healthy)
_captureDescription = description;
else
Expand Down Expand Up @@ -1649,6 +1744,7 @@
OverlayTone.Neutral,
30_000);
string path = await SaveReplayGuardedAsync(engine);
_settingsWindow?.NotifyReplaySaved(path);
ShowOverlayNotification(
"✓",
Localization.Text("L.Notify.Saved"),
Expand Down Expand Up @@ -1703,7 +1799,18 @@
await RunOnObsThreadAsync(engine.ResetReplayWindow)
.ConfigureAwait(false);
}
return path;
try
{
return ReplayPaths.RouteSavedReplay(
_config!,
path,
operation.GameExecutable);
}
catch (Exception exception)
{
Log.Write($"Could not route replay into game folder: {exception}");
return path;
}
}
finally
{
Expand Down
16 changes: 15 additions & 1 deletion src/Captail/Captail.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<Product>Captail</Product>
<RootNamespace>Captail</RootNamespace>
<ApplicationIcon>Assets\Captail.ico</ApplicationIcon>
<Version>0.1.3</Version>
<Version>0.1.4</Version>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>

Expand Down Expand Up @@ -43,7 +43,9 @@

<PropertyGroup>
<ObsRuntimeRoot>$(MSBuildProjectDirectory)\..\..\runtime\obs</ObsRuntimeRoot>
<FfmpegRuntimeRoot>$(MSBuildProjectDirectory)\..\..\runtime\ffmpeg</FfmpegRuntimeRoot>
<AcquireObsRuntimeOnBuild Condition="'$(AcquireObsRuntimeOnBuild)' == ''">true</AcquireObsRuntimeOnBuild>
<AcquireFfmpegRuntimeOnBuild Condition="'$(AcquireFfmpegRuntimeOnBuild)' == ''">true</AcquireFfmpegRuntimeOnBuild>
<ObsBridgeSource>$(MSBuildProjectDirectory)\..\..\native\ObsBridge</ObsBridgeSource>
<ObsBridgeBuild>$(ObsBridgeSource)\build</ObsBridgeBuild>
</PropertyGroup>
Expand All @@ -54,6 +56,12 @@
<Exec Command="powershell -NoProfile -ExecutionPolicy Bypass -File &quot;$(MSBuildProjectDirectory)\..\..\tools\AcquireObsRuntime.ps1&quot;" />
</Target>

<Target Name="AcquireFfmpegRuntime"
BeforeTargets="PrepareForBuild"
Condition="'$(AcquireFfmpegRuntimeOnBuild)' == 'true' and !Exists('$(FfmpegRuntimeRoot)\ffmpeg.exe')">
<Exec Command="powershell -NoProfile -ExecutionPolicy Bypass -File &quot;$(MSBuildProjectDirectory)\..\..\tools\AcquireFfmpegRuntime.ps1&quot;" />
</Target>

<Target Name="BuildObsBridge" BeforeTargets="Build">
<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" />
Expand Down Expand Up @@ -102,6 +110,12 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="$(FfmpegRuntimeRoot)\**\*">
<Link>ffmpeg\%(RecursiveDir)%(Filename)%(Extension)</Link>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>

</Project>
Loading
Loading