From 75daa19696741efb3df3375960e40b2cc2494f69 Mon Sep 17 00:00:00 2001 From: FaulMit <48646918+FaulMit@users.noreply.github.com> Date: Sun, 26 Jul 2026 16:13:18 +0200 Subject: [PATCH] Release Captail 0.1.3 reliability and updates --- CHANGELOG.md | 13 + README.md | 23 +- docs/captail-main.jpg | Bin 17072 -> 17210 bytes docs/captail-settings-audio.jpg | Bin 0 -> 32825 bytes docs/captail-settings-video.jpg | Bin 0 -> 29643 bytes src/Captail/App.xaml.cs | 251 ++++++++- src/Captail/Captail.csproj | 2 +- src/Captail/Languages/Strings.en.xaml | 26 + src/Captail/Languages/Strings.ru.xaml | 26 + src/Captail/ObsReplayEngine.cs | 108 +++- src/Captail/SettingsWindow.xaml | 187 ++++++- src/Captail/SettingsWindow.xaml.cs | 259 +++++++++- src/Captail/Themes/Theme.xaml | 191 +++++++ src/Captail/UpdateService.cs | 700 ++++++++++++++++++++++++++ 14 files changed, 1723 insertions(+), 63 deletions(-) create mode 100644 docs/captail-settings-audio.jpg create mode 100644 docs/captail-settings-video.jpg create mode 100644 src/Captail/UpdateService.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index b763999..2925937 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable user-facing changes are documented here. ## [Unreleased] +## [0.1.3] - 2026-07-26 + +### Added + +- Concise English and Russian help tooltips for replay, video, and audio settings. +- Dashboard footer with repository access, current version, and one-click updates for installed and Portable builds. + +### Fixed + +- Watchdog checks no longer mistake an in-progress pipeline startup for a stopped recording module. +- Consecutive replay saves now advance the rolling window so later clips contain only footage recorded since the previous save. +- Save controls and notifications now show the currently available replay duration instead of always showing the configured maximum. + ## [0.1.2] - 2026-07-22 ### Fixed diff --git a/README.md b/README.md index 281653c..c29fd78 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,28 @@ 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. +## Interface +

Captail main window showing replay status, audio sources, recording format, disk space, and save hotkey

+

+ Captail video settings with source, codec, bitrate, resolution, and frame-rate controls + Captail audio, storage, and hotkey settings +

+ > [!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 + +- **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. + ## Why Captail? - **Small, focused UI** — replay state, active source, audio, hotkey, and disk space at a glance. @@ -53,6 +68,8 @@ Download the latest version from [GitHub Releases](https://github.com/FaulMit/ca Both packages are self-contained. **OBS Studio and .NET do not need to be installed separately.** +Captail checks GitHub Releases in the background. When an update is available, the version in the footer becomes a mint update button. Click it to download, verify, install, and restart. Installer builds update through Setup; Portable builds replace their extracted files without changing package type. + > [!NOTE] > Current binaries are not Authenticode-signed. Windows SmartScreen may show an “Unknown publisher” warning. Verify the SHA-256 checksum and GitHub build provenance before running a release. @@ -104,6 +121,8 @@ Both hotkeys can be changed in Settings. - Start with Windows. - Native overlay notifications for replay state and saved clips. - Single-instance protection. +- Built-in GitHub release indicator and verified one-click updates. +- Direct repository link in the footer. - English interface by default, with live English/Russian switching. ### Reliability @@ -111,6 +130,7 @@ Both hotkeys can be changed in Settings. - Recording-pipeline watchdog. - Automatic replay restart after recoverable capture or encoder failure. - Progressive retry delay when the graphics driver is temporarily unavailable. +- Sequential save boundaries prevent duplicate footage across consecutive clips. - Fragmented MP4 output to reduce the risk of losing an entire file after interruption. - Protected/DRM surfaces may become black while the rolling buffer continues. @@ -144,7 +164,7 @@ Feedback is especially valuable during this preview. 1. Check [existing issues](https://github.com/FaulMit/captail/issues). 2. Open a [bug report](https://github.com/FaulMit/captail/issues/new/choose). 3. Include Captail version, Windows version, GPU, driver version, source, codec, FPS, resolution, and reproduction steps. -4. Attach `%APPDATA%\Captail\log.txt` when possible. Review it first and remove personal paths or other sensitive information. +4. Attach `%LOCALAPPDATA%\Captail\log.txt` when possible. Review it first and remove personal paths or other sensitive information. Requests for older NVIDIA, AMD, and Intel testing are welcome even when everything works. @@ -180,6 +200,7 @@ 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. +- `UpdateService.cs` — GitHub release discovery, package verification, Installer updates, and Portable self-replacement. ## License and attribution diff --git a/docs/captail-main.jpg b/docs/captail-main.jpg index c43d7d8ea84c921bd261a578a2cb6888d700854b..237557fc6c6f849fdafa2f8f88cf55e44a4d4bc1 100644 GIT binary patch literal 17210 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 literal 17072 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 pLau%zAr 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 LwI2$7`}}_ZmX`%q literal 0 HcmV?d00001 diff --git a/docs/captail-settings-video.jpg b/docs/captail-settings-video.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1e207ab66964b31b55c3ae6e5b33a553dd40b0b5 GIT binary patch literal 29643 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 literal 0 HcmV?d00001 diff --git a/src/Captail/App.xaml.cs b/src/Captail/App.xaml.cs index dc6e8ef..fd943cf 100644 --- a/src/Captail/App.xaml.cs +++ b/src/Captail/App.xaml.cs @@ -36,6 +36,8 @@ public partial class App : Application private int _recoveryFailures; private int _recoveryInProgress; private OverlayNotificationWindow? _overlayNotification; + private readonly UpdateService _updateService = new(); + private DispatcherTimer? _updateShutdownTimer; private int _saving; private EncoderCapabilities? _capabilities; private readonly SemaphoreSlim _pipelineGate = new(1, 1); @@ -45,6 +47,9 @@ public partial class App : Application private string? _captureDescription; private int _exiting; private bool _shutdownExistingSucceeded = true; +#if DEBUG + private bool _qaUpdateAvailable; +#endif private bool IsReplayRunning => _replayRunning; @@ -69,11 +74,22 @@ protected override async void OnStartup(StartupEventArgs e) argument => argument.StartsWith( "--qa-game-capture=", StringComparison.OrdinalIgnoreCase)); + bool replaySegmentsTest = e.Args.Contains( + "--qa-replay-segments", + StringComparer.OrdinalIgnoreCase); + bool updateCheckTest = e.Args.Contains( + "--qa-update-check", + StringComparer.OrdinalIgnoreCase); + _qaUpdateAvailable = e.Args.Contains( + "--qa-update-available", + StringComparer.OrdinalIgnoreCase); #else const bool faultTest = false; const bool codecTest = false; const bool capabilityModelTest = false; const bool gameCaptureTest = false; + const bool replaySegmentsTest = false; + const bool updateCheckTest = false; #endif bool backgroundLaunch = e.Args.Contains( "--background", @@ -85,7 +101,7 @@ protected override async void OnStartup(StartupEventArgs e) backgroundLaunch, shutdownExisting, _uiOnly || faultTest || codecTest || capabilityModelTest || - gameCaptureTest)) + gameCaptureTest || replaySegmentsTest || updateCheckTest)) { Shutdown(); return; @@ -134,6 +150,19 @@ protected override async void OnStartup(StartupEventArgs e) RunGameCaptureTest(e.Args); return; } + if (replaySegmentsTest) + { + RunReplaySegmentsTest(); + return; + } + if (updateCheckTest) + { + await _updateService.CheckAsync( + force: true, + CancellationToken.None); + Shutdown(0); + return; + } #endif if (_uiOnly) { @@ -917,34 +946,42 @@ private async Task MonitorPipelineAsync() return; } - if (_obs is null) - { - await RecoverPipelineAsync(Localization.Text("L.Recovery.ModuleStopped")); - return; - } - if (DateTime.UtcNow - _pipelineStartedUtc < TimeSpan.FromSeconds(8)) - return; if (!await _pipelineGate.WaitAsync(0)) return; - bool healthy; + string? recoveryReason = null; try { ObsReplayEngine? engine = _obs; - (healthy, string? description) = engine is null - ? (false, null) - : await RunOnObsThreadAsync(() => - (engine.IsHealthy, engine.Description)); - if (healthy) - _captureDescription = description; + if (engine is null) + { + recoveryReason = Localization.Text( + "L.Recovery.ModuleStopped"); + } + else if (DateTime.UtcNow - _pipelineStartedUtc < + TimeSpan.FromSeconds(8)) + { + return; + } + else + { + (bool healthy, string? description) = + await RunOnObsThreadAsync(() => + (engine.IsHealthy, engine.Description)); + if (healthy) + _captureDescription = description; + else + recoveryReason = Localization.Text( + "L.Recovery.NoFrames"); + } } finally { _pipelineGate.Release(); } - if (!healthy) - await RecoverPipelineAsync(Localization.Text("L.Recovery.NoFrames")); + if (recoveryReason is not null) + await RecoverPipelineAsync(recoveryReason); else UpdateUiState(); } @@ -1145,7 +1182,9 @@ private void OpenSettings() SetReplayEnabledAsync, SetAudioSourcesAsync, ApplySettingsAsync, - capabilities); + capabilities, + CheckForUpdatesAsync, + PrepareAndLaunchUpdateAsync); _settingsWindow.Closed += (_, _) => { _settingsWindow = null; @@ -1169,6 +1208,58 @@ private void OpenSettings() } } + private Task CheckForUpdatesAsync( + bool force, + CancellationToken cancellationToken) + { +#if DEBUG + if (_qaUpdateAvailable) + { + var asset = new UpdateAsset( + "qa", + new Uri("https://github.com/FaulMit/captail"), + 1, + $"sha256:{new string('0', 64)}"); + return Task.FromResult( + new UpdateRelease( + new Version(0, 2, 0), + "v0.2.0", + new Uri( + "https://github.com/FaulMit/captail/releases/tag/v0.2.0"), + true, + asset, + asset, + null)); + } +#endif + return _updateService.CheckAsync(force, cancellationToken); + } + + private async Task PrepareAndLaunchUpdateAsync( + UpdateRelease release, + IProgress progress, + CancellationToken cancellationToken) + { + PreparedUpdate update = await _updateService.PrepareAsync( + release, + progress, + cancellationToken); + UpdateService.Launch(update); + + _updateShutdownTimer?.Stop(); + _updateShutdownTimer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(450), + }; + _updateShutdownTimer.Tick += (_, _) => + { + _updateShutdownTimer?.Stop(); + _updateShutdownTimer = null; + _ = RequestShutdownAsync(); + }; + _updateShutdownTimer.Start(); + } + private async Task EnsureCapabilitiesAsync() { if (_capabilities is not null && @@ -1494,12 +1585,16 @@ private void UpdateUiState() { bool active = IsReplayRunning; string codec = _obs?.ActiveCodec ?? _config?.Codec ?? "h264"; + int availableReplaySeconds = active + ? _obs?.AvailableReplaySeconds ?? _config!.BufferSeconds + : 0; if (_capabilities is not null) _settingsWindow?.UpdateCapabilities(_capabilities); _settingsWindow?.UpdateRuntimeState( active, codec, - _captureDescription); + _captureDescription, + availableReplaySeconds); if (_tray is not null) { _tray.ToolTipText = active @@ -1537,6 +1632,11 @@ private async Task SaveReplayCoreAsync(ObsReplayEngine engine) { try { + int availableReplaySeconds = Math.Max( + 1, + Math.Min( + _config!.BufferSeconds, + engine.AvailableReplaySeconds)); // Shown immediately so the user sees progress; replaced by the result // notification once the file is on disk. The long duration is a safety // net — the "saved"/"failed" notification supersedes it well before then. @@ -1545,7 +1645,7 @@ private async Task SaveReplayCoreAsync(ObsReplayEngine engine) Localization.Text("L.Notify.Saving"), Localization.Format( "L.Notify.SavingDetail", - FormatDuration(_config!.BufferSeconds)), + FormatDuration(availableReplaySeconds)), OverlayTone.Neutral, 30_000); string path = await SaveReplayGuardedAsync(engine); @@ -1581,10 +1681,29 @@ private async Task SaveReplayGuardedAsync(ObsReplayEngine engine) throw new InvalidOperationException( Localization.Text("L.Notify.EnableBeforeSave")); } - Task saveOperation = await RunOnObsThreadAsync( - () => engine.SaveReplayAsync()) + ReplaySaveOperation operation = await RunOnObsThreadAsync( + () => engine.BeginSaveReplay()) + .ConfigureAwait(false); + bool snapshotStarted = await WaitForSaveSnapshotAsync( + engine, + operation) .ConfigureAwait(false); - return await saveOperation.ConfigureAwait(false); + if (snapshotStarted) + { + await RunOnObsThreadAsync(engine.ResetReplayWindow) + .ConfigureAwait(false); + } + + string path = await operation.Completion.ConfigureAwait(false); + if (!snapshotStarted) + { + Log.Write( + "Replay snapshot marker was delayed; advancing window " + + "after mux completion."); + await RunOnObsThreadAsync(engine.ResetReplayWindow) + .ConfigureAwait(false); + } + return path; } finally { @@ -1592,6 +1711,91 @@ private async Task SaveReplayGuardedAsync(ObsReplayEngine engine) } } +#if DEBUG + private async void RunReplaySegmentsTest() + { + try + { + string root = Path.Combine( + Path.GetTempPath(), + "Captail", + $"obs_segments_{Environment.ProcessId}"); + _config = new Config + { + ReplayEnabled = true, + BufferSeconds = 15, + FrameRate = 30, + BitrateMbps = 8, + Codec = "h264", + CaptureSource = "desktop", + CaptureSystemAudio = false, + CaptureMicrophone = false, + OutputDirectory = root, + }; + if (!await TryStartPipelineAsync(showError: false)) + { + throw new InvalidOperationException( + "The replay segment pipeline did not start."); + } + + await Task.Delay(TimeSpan.FromSeconds(6)); + string first = await SaveReplayGuardedAsync(_obs!); + int availableAfterFirst = _obs!.AvailableReplaySeconds; + await Task.Delay(TimeSpan.FromSeconds(3)); + string second = await SaveReplayGuardedAsync(_obs); + int availableAfterSecond = _obs.AvailableReplaySeconds; + + bool passed = + File.Exists(first) && + File.Exists(second) && + new FileInfo(first).Length > 0 && + new FileInfo(second).Length > 0 && + availableAfterFirst <= 1 && + availableAfterSecond <= 1; + Log.Write( + $"OBS_SEGMENT_TEST {(passed ? "PASS" : "FAIL")}: " + + $"first={first}, second={second}, " + + $"availableAfterFirst={availableAfterFirst}s, " + + $"availableAfterSecond={availableAfterSecond}s"); + await StopPipelineCoreAsync(); + Shutdown(passed ? 0 : 13); + } + catch (Exception exception) + { + Log.Write($"OBS_SEGMENT_TEST FAIL: {exception}"); + Shutdown(13); + } + } +#endif + + private async Task WaitForSaveSnapshotAsync( + ObsReplayEngine engine, + ReplaySaveOperation operation) + { + DateTime deadline = DateTime.UtcNow.AddSeconds(5); + while (DateTime.UtcNow < deadline) + { + if (!ReferenceEquals(engine, _obs) || + !IsReplayRunning || + Volatile.Read(ref _exiting) != 0) + { + throw new InvalidOperationException( + Localization.Text("L.Notify.EnableBeforeSave")); + } + + bool started = await RunOnObsThreadAsync( + () => engine.HasSaveSnapshotStarted(operation)) + .ConfigureAwait(false); + if (started) + return true; + if (operation.Completion.IsCompleted) + break; + await Task.Delay(15).ConfigureAwait(false); + } + + return false; + } + private void OnLanguageChanged() { if (!Dispatcher.CheckAccess()) @@ -1668,6 +1872,7 @@ protected override void OnExit(ExitEventArgs e) Interlocked.Exchange(ref _exiting, 1) != 0 && _obs is null; Localization.Changed -= OnLanguageChanged; _healthTimer?.Stop(); + _updateShutdownTimer?.Stop(); _activationServerCts?.Cancel(); _activationServerCts?.Dispose(); _settingsWindow?.Close(); diff --git a/src/Captail/Captail.csproj b/src/Captail/Captail.csproj index e06d31a..03ef977 100644 --- a/src/Captail/Captail.csproj +++ b/src/Captail/Captail.csproj @@ -14,7 +14,7 @@ Captail Captail Assets\Captail.ico - 0.1.2 + 0.1.3 true diff --git a/src/Captail/Languages/Strings.en.xaml b/src/Captail/Languages/Strings.en.xaml index 0b7a40f..db77f63 100644 --- a/src/Captail/Languages/Strings.en.xaml +++ b/src/Captail/Languages/Strings.en.xaml @@ -10,6 +10,20 @@ Done Minimize Close + Open Captail on GitHub + Version {0} · click to check for updates + Checking… + Checking GitHub Releases + Captail {0} is up to date · click to check again + Update v{0} + Version {0} is ready · click to download and update + Downloading {0}% + Downloading and verifying Captail {0} + Installing… + Captail will restart after the update + Could not check for updates · click to retry + Update check failed + Update failed Instant Replay is on Instant Replay is off @@ -59,6 +73,8 @@ Maximum compressed video and audio packet memory Limits RAM use and duration Duration only + How much recent footage Captail keeps ready. Longer buffers use more RAM. After a save, the next clip starts with new footage instead of repeating the previous clip. + Hard limit for compressed replay data in RAM. If this limit is reached first, the available replay will be shorter. Leave “Duration only” unless you need predictable memory use. 250 MB 500 MB 1 GB @@ -90,6 +106,13 @@ source {0} · not running Choose a running game… + Desktop captures the selected monitor; protected video may appear black without stopping the buffer. Game Capture hooks the selected game directly for cleaner high-FPS recording. + AV1 — newest and most efficient. Cleaner at the same bitrate, or smaller files at the same quality. Hardware support includes GeForce RTX 40/50, Radeon RX 7000 and Intel Arc. + HEVC — nearly as compact and supported by more modern GPUs. + H.264 — uses more space, but opens almost everywhere. + How much video data is written each second. Higher values keep fast motion cleaner, but use more RAM and disk space. 50 Mbps is roughly 375 MB per minute. Auto adapts to codec, resolution and FPS. + Source keeps the original dimensions. Lower resolution reduces GPU load, RAM use and file size, but also removes detail. It does not change monitor resolution. + 60 FPS is enough for normal playback. 120–240 FPS records real extra frames for smooth slow motion; Captail does not invent frames. The game must render fast enough, and higher FPS increases GPU and memory use. System audio Game audio @@ -102,6 +125,9 @@ One mixed track Separate tracks Audio codec + Raises microphone gain before encoding. Use it when 100% volume is still quiet. Too much boost also raises noise and can make loud speech distort. + Mixed track plays everywhere and is easiest to share. Separate tracks keep game/system audio and microphone independent for editing; some players play only one track at a time. + AAC saves as MP4 and works with almost every player and editor. Opus is efficient and sounds good at lower bitrates, but uses MKV and may need a modern editor. Replay folder Change… diff --git a/src/Captail/Languages/Strings.ru.xaml b/src/Captail/Languages/Strings.ru.xaml index 4ab406d..82bb287 100644 --- a/src/Captail/Languages/Strings.ru.xaml +++ b/src/Captail/Languages/Strings.ru.xaml @@ -10,6 +10,20 @@ Готово Свернуть Закрыть + Открыть Captail на GitHub + Версия {0} · нажмите, чтобы проверить обновления + Проверка… + Проверяем GitHub Releases + Установлена актуальная версия {0} · нажмите для повторной проверки + Обновить до v{0} + Доступна версия {0} · нажмите, чтобы скачать и обновить + Загрузка {0}% + Скачиваем и проверяем Captail {0} + Установка… + После обновления Captail перезапустится + Не удалось проверить обновления · нажмите, чтобы повторить + Ошибка проверки обновлений + Ошибка обновления Мгновенный повтор включён Мгновенный повтор выключен @@ -59,6 +73,8 @@ Максимальный объём сжатых видео- и аудиопакетов в оперативной памяти Ограничивает ОЗУ и длительность Только длительность + Сколько последних секунд Captail держит наготове. Чем длиннее буфер, тем больше нужно ОЗУ. После сохранения следующий клип начинается с новых кадров и не повторяет предыдущий. + Жёсткий лимит сжатого буфера в ОЗУ. Если память закончится раньше времени, доступный повтор станет короче. Оставьте «Только длительность», если точный расход памяти не важен. 250 МБ 500 МБ 1 ГБ @@ -90,6 +106,13 @@ исходное {0} · не запущена Выберите запущенную игру… + «Рабочий стол» записывает выбранный монитор; защищённое видео может быть чёрным, но буфер продолжит работу. Game Capture напрямую захватывает выбранную игру и лучше подходит для высокого FPS. + AV1 — самый современный и эффективный. Чище при том же битрейте или меньше файл при том же качестве. Поддержка есть на GeForce RTX 40/50, Radeon RX 7000 и Intel Arc. + HEVC — почти такой же компактный и поддерживается большим числом современных GPU. + H.264 — занимает больше места, зато открывается почти везде. + Сколько видеоданных записывается каждую секунду. Чем выше значение, тем чище быстрое движение, но больше расход ОЗУ и диска. 50 Мбит/с — примерно 375 МБ в минуту. «Авто» учитывает кодек, разрешение и FPS. + «Как у источника» сохраняет исходный размер кадра. Меньшее разрешение снижает нагрузку, расход ОЗУ и размер файла, но убирает детали. Разрешение монитора не меняется. + 60 FPS достаточно для обычного просмотра. 120–240 FPS записывают реальные дополнительные кадры для плавного замедления — Captail не дорисовывает фейковые. Игра должна выдавать нужный FPS; нагрузка и расход памяти растут. Звук рабочего стола Звук игры @@ -102,6 +125,9 @@ Одна общая Раздельные Аудиокодек + Усиливает микрофон до кодирования. Используйте, если даже на 100% голос тихий. Слишком высокое усиление также поднимает шум и может перегружать громкую речь. + Общая дорожка работает почти везде и удобна для просмотра. Раздельные сохраняют игру/систему и микрофон отдельно для монтажа; некоторые плееры воспроизводят только одну дорожку за раз. + AAC сохраняется в MP4 и работает почти со всеми плеерами и редакторами. Opus эффективнее на низком битрейте, но использует MKV и может потребовать современный редактор. Папка повторов Изменить… diff --git a/src/Captail/ObsReplayEngine.cs b/src/Captail/ObsReplayEngine.cs index 83efade..21eb791 100644 --- a/src/Captail/ObsReplayEngine.cs +++ b/src/Captail/ObsReplayEngine.cs @@ -38,8 +38,10 @@ public sealed class ObsReplayEngine : IDisposable private bool _obsStarted; private bool _logBridgeInstalled; private bool _disposing; + private bool _resettingReplayWindow; private uint _previousFrameCount; private DateTime _previousFrameCheckUtc; + private DateTime _replayWindowStartedUtc; private uint _outputWidth; private uint _outputHeight; private uint _baseWidth; @@ -108,6 +110,18 @@ public bool IsHealthy public ulong BufferedBytes => _output == 0 ? 0 : ObsNative.obs_output_get_total_bytes(_output); + public int AvailableReplaySeconds + { + get + { + if (!IsActive || _replayWindowStartedUtc == default) + return 0; + + int elapsed = (int)Math.Floor( + (DateTime.UtcNow - _replayWindowStartedUtc).TotalSeconds); + return Math.Clamp(elapsed, 0, _config.BufferSeconds); + } + } public uint TotalRenderedFrames => ObsNative.obs_get_total_frames(); public uint LaggedRenderedFrames => ObsNative.obs_get_lagged_frames(); @@ -182,21 +196,23 @@ public static EncoderCapabilities ProbeCapabilities(Config config) } using (probe) - try - { - probe.InitializeObs(); - return probe.DetectCapabilities(); - } - catch (Exception exception) - { - Log.Write($"GPU capability detection failed: {exception}"); - return EncoderCapabilities.Failed(exception.Message); - } + try + { + probe.InitializeObs(); + return probe.DetectCapabilities(); + } + catch (Exception exception) + { + Log.Write($"GPU capability detection failed: {exception}"); + return EncoderCapabilities.Failed(exception.Message); + } } - public async Task SaveReplayAsync(CancellationToken cancellationToken = default) + public ReplaySaveOperation BeginSaveReplay( + CancellationToken cancellationToken = default) { Task completion; + ulong initialMuxBytes; lock (_saveGate) { if (!IsActive) @@ -209,6 +225,7 @@ public async Task SaveReplayAsync(CancellationToken cancellationToken = _pendingSave = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); completion = _pendingSave.Task; + initialMuxBytes = BufferedBytes; nint procedures = ObsNative.obs_output_get_proc_handler(_output); if (procedures == 0 || !ObsNative.proc_handler_call(procedures, "save", 0)) @@ -219,6 +236,68 @@ public async Task SaveReplayAsync(CancellationToken cancellationToken = } } + return new ReplaySaveOperation( + WaitForSaveCompletionAsync(completion, cancellationToken), + initialMuxBytes); + } + + public Task SaveReplayAsync( + CancellationToken cancellationToken = default) => + BeginSaveReplay(cancellationToken).Completion; + + public bool HasSaveSnapshotStarted(ReplaySaveOperation operation) => + BufferedBytes != operation.InitialMuxBytes; + + public void ResetReplayWindow() + { + if (!IsActive) + throw new InvalidOperationException( + Localization.Text("L.Engine.BufferStopped")); + + _resettingReplayWindow = true; + try + { + ObsNative.obs_output_stop(_output); + for (int attempt = 0; + attempt < 80 && ObsNative.obs_output_active(_output); + attempt++) + { + Thread.Sleep(25); + } + if (ObsNative.obs_output_active(_output)) + { + ObsNative.obs_output_force_stop(_output); + for (int attempt = 0; + attempt < 40 && ObsNative.obs_output_active(_output); + attempt++) + { + Thread.Sleep(25); + } + } + if (ObsNative.obs_output_active(_output) || + !ObsNative.obs_output_start(_output)) + { + string error = PtrToString( + ObsNative.obs_output_get_last_error(_output)); + throw new InvalidOperationException( + string.IsNullOrWhiteSpace(error) + ? Localization.Text("L.Engine.BufferStartFailed") + : error); + } + + _replayWindowStartedUtc = DateTime.UtcNow; + Log.Write("Replay window advanced after save."); + } + finally + { + _resettingReplayWindow = false; + } + } + + private async Task WaitForSaveCompletionAsync( + Task completion, + CancellationToken cancellationToken) + { using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeout.CancelAfter(TimeSpan.FromSeconds(45)); try @@ -920,6 +999,7 @@ private void CreateReplayBuffer() ? Localization.Text("L.Engine.BufferStartFailed") : error); } + _replayWindowStartedUtc = DateTime.UtcNow; } private int AudioTrackCount() @@ -952,7 +1032,7 @@ private void OnReplaySaved(nint _, nint __) private void OnOutputStopped(nint _, nint __) { - if (_disposing) + if (_disposing || _resettingReplayWindow) return; string error = PtrToString(ObsNative.obs_output_get_last_error(_output)); Faulted?.Invoke( @@ -1131,3 +1211,7 @@ public void Dispose() _contextOwned = false; } } + +public sealed record ReplaySaveOperation( + Task Completion, + ulong InitialMuxBytes); diff --git a/src/Captail/SettingsWindow.xaml b/src/Captail/SettingsWindow.xaml index 6b8c16a..a3d424f 100644 --- a/src/Captail/SettingsWindow.xaml +++ b/src/Captail/SettingsWindow.xaml @@ -24,6 +24,7 @@ + - + + + + @@ -262,9 +275,20 @@ - + + + + @@ -297,7 +321,19 @@ - + + + + @@ -328,7 +364,21 @@ - + + + + - + + + + @@ -375,7 +437,19 @@ - + + + + @@ -387,7 +461,19 @@ - + + + + @@ -450,13 +536,24 @@ - + - + + + + - + + + + @@ -489,7 +598,19 @@ - + + + + @@ -565,7 +686,41 @@ - + + + + + + + > _setReplayEnabled; private readonly Func> _setAudioSources; private readonly Func> _applySettings; + private readonly Func> + _checkForUpdates; + private readonly Func< + UpdateRelease, + IProgress, + CancellationToken, + Task> _installUpdate; private EncoderCapabilities _capabilities; private readonly DispatcherTimer _diskTimer; private readonly CancellationTokenSource _lifetimeCts = new(); @@ -28,22 +35,34 @@ public partial class SettingsWindow : Window private Button? _capturingHotkeyButton; private bool _updatingUi; private bool _runtimeActive; + private int _availableReplaySeconds; private bool? _animatedRecordingState; private int _deviceRefreshVersion; private int _processRefreshVersion; private int _diskRefreshInProgress; private int _actionInProgress; + private UpdateRelease? _availableUpdate; + private UpdateDisplayState _updateDisplayState = UpdateDisplayState.Current; + private int _updateProgress; + private bool _updateCheckInProgress; + private bool _updateInstallInProgress; public bool Applied { get; private set; } - public SettingsWindow( + internal SettingsWindow( Config config, bool runtimeActive, Action saveReplay, Func> setReplayEnabled, Func> setAudioSources, Func> applySettings, - EncoderCapabilities capabilities) + EncoderCapabilities capabilities, + Func> checkForUpdates, + Func< + UpdateRelease, + IProgress, + CancellationToken, + Task> installUpdate) { _config = config; _saveReplay = saveReplay; @@ -51,6 +70,8 @@ public SettingsWindow( _setAudioSources = setAudioSources; _applySettings = applySettings; _capabilities = capabilities; + _checkForUpdates = checkForUpdates; + _installUpdate = installUpdate; _outputDirectory = config.OutputDirectory; _pendingSaveHotkey = config.Hotkey; _pendingToggleHotkey = config.ToggleReplayHotkey; @@ -72,13 +93,18 @@ public SettingsWindow( ResetDeviceLists(); LoadSettingsControls(); UpdateRuntimeState(runtimeActive); - Loaded += async (_, _) => await RunUiActionAsync(async () => + RenderUpdateStatus(); + Loaded += async (_, _) => { - await Task.WhenAll( - LoadDeviceListsAsync(), - PopulateGameProcessesAsync(), - RefreshDiskAsync()); - }); + await RunUiActionAsync(async () => + { + await Task.WhenAll( + LoadDeviceListsAsync(), + PopulateGameProcessesAsync(), + RefreshDiskAsync()); + }); + _ = CheckForUpdatesAsync(force: false); + }; _diskTimer.Start(); } @@ -438,9 +464,14 @@ private void LoadSettingsControls() public void UpdateRuntimeState( bool active, string? activeCodec = null, - string? activeCaptureSource = null) + string? activeCaptureSource = null, + int? availableReplaySeconds = null) { _runtimeActive = active; + if (availableReplaySeconds is not null) + _availableReplaySeconds = availableReplaySeconds.Value; + else if (!active) + _availableReplaySeconds = 0; _updatingUi = true; ReplayToggle.IsChecked = active; SettingsReplayToggle.IsChecked = active; @@ -492,7 +523,10 @@ public void UpdateRuntimeState( FpsSummaryText.Text = $"{_config.FrameRate} FPS"; SaveButtonText.Text = Localization.Format( "L.Save.Duration", - FormatDuration(_config.BufferSeconds)); + FormatDuration( + active + ? Math.Max(1, _availableReplaySeconds) + : _config.BufferSeconds)); HotkeySummaryText.Text = _config.Hotkey; OutputFolderSummaryText.Text = _config.OutputDirectory; } @@ -559,9 +593,204 @@ private void OnLanguageChanged() ApplyHardwareCapabilities(); UpdateCaptureSourceState(); UpdateRuntimeState(_runtimeActive); + RenderUpdateStatus(); _ = RefreshDiskAsync(); } + private void GitHub_Click(object sender, RoutedEventArgs e) + { + try + { + Process.Start(new ProcessStartInfo + { + FileName = UpdateService.RepositoryUrl, + UseShellExecute = true, + }); + AnimatePress(GitHubButton); + } + catch (Exception exception) + { + HandleUiActionError("Open GitHub repository", exception); + } + } + + private async void UpdateVersion_Click( + object sender, + RoutedEventArgs e) + { + if (_updateCheckInProgress || _updateInstallInProgress) + return; + + AnimatePress(UpdateVersionButton); + if (_availableUpdate is null) + { + await CheckForUpdatesAsync(force: true); + return; + } + + _updateInstallInProgress = true; + _updateDisplayState = UpdateDisplayState.Downloading; + _updateProgress = 0; + RenderUpdateStatus(); + + try + { + var progress = new Progress(value => + { + _updateProgress = Math.Clamp(value, 0, 100); + RenderUpdateStatus(); + }); + await _installUpdate( + _availableUpdate, + progress, + _lifetimeCts.Token); + _updateDisplayState = UpdateDisplayState.Installing; + RenderUpdateStatus(); + } + catch (OperationCanceledException) + when (_lifetimeCts.IsCancellationRequested) + { + // Window is closing. + } + catch (Exception exception) + { + Log.Write($"Update installation failed: {exception}"); + _updateInstallInProgress = false; + _updateDisplayState = UpdateDisplayState.Available; + RenderUpdateStatus(); + ShowError( + Localization.Text("L.Update.InstallFailedTitle"), + exception.Message); + } + } + + private async Task CheckForUpdatesAsync(bool force) + { + if (_updateCheckInProgress || _updateInstallInProgress) + return; + + _updateCheckInProgress = true; + _updateDisplayState = UpdateDisplayState.Checking; + RenderUpdateStatus(); + try + { + _availableUpdate = await _checkForUpdates( + force, + _lifetimeCts.Token); + _updateDisplayState = _availableUpdate is null + ? UpdateDisplayState.Current + : UpdateDisplayState.Available; + RenderUpdateStatus(); + if (_availableUpdate is not null) + { + AnimatePress(UpdateVersionButton); + var pulse = new DoubleAnimation( + 0.28, + 1, + TimeSpan.FromMilliseconds(420)) + { + AutoReverse = true, + RepeatBehavior = new RepeatBehavior(2), + }; + UpdateStatusDot.BeginAnimation( + OpacityProperty, + pulse); + } + } + catch (OperationCanceledException) + when (_lifetimeCts.IsCancellationRequested) + { + // Window is closing. + } + catch (Exception exception) + { + Log.Write($"Update check failed: {exception}"); + _availableUpdate = null; + _updateDisplayState = UpdateDisplayState.CheckFailed; + RenderUpdateStatus(); + if (force) + { + ShowError( + Localization.Text("L.Update.CheckFailedTitle"), + exception.Message); + } + } + finally + { + _updateCheckInProgress = false; + if (!_lifetimeCts.IsCancellationRequested) + RenderUpdateStatus(); + } + } + + private void RenderUpdateStatus() + { + string current = UpdateService.CurrentVersionText; + string? available = _availableUpdate is null + ? null + : UpdateService.FormatVersion(_availableUpdate.Version); + + UpdateVersionButton.Tag = _updateDisplayState switch + { + UpdateDisplayState.Available => "available", + UpdateDisplayState.Checking or + UpdateDisplayState.Downloading or + UpdateDisplayState.Installing => "busy", + _ => "current", + }; + UpdateStatusDot.Visibility = _updateDisplayState is + UpdateDisplayState.Available or + UpdateDisplayState.Downloading or + UpdateDisplayState.Installing + ? Visibility.Visible + : Visibility.Collapsed; + + switch (_updateDisplayState) + { + case UpdateDisplayState.Checking: + UpdateStatusText.Text = + $"v{current} · {Localization.Text("L.Update.Checking")}"; + UpdateVersionButton.ToolTip = + Localization.Text("L.Update.CheckingTip"); + break; + case UpdateDisplayState.Available: + UpdateStatusText.Text = Localization.Format( + "L.Update.Available", + available ?? current); + UpdateVersionButton.ToolTip = Localization.Format( + "L.Update.AvailableTip", + available ?? current); + break; + case UpdateDisplayState.Downloading: + UpdateStatusText.Text = Localization.Format( + "L.Update.Downloading", + _updateProgress); + UpdateVersionButton.ToolTip = Localization.Format( + "L.Update.DownloadingTip", + available ?? current); + break; + case UpdateDisplayState.Installing: + UpdateStatusText.Text = + Localization.Text("L.Update.Installing"); + UpdateVersionButton.ToolTip = + Localization.Text("L.Update.InstallingTip"); + break; + case UpdateDisplayState.CheckFailed: + UpdateStatusText.Text = $"v{current}"; + UpdateVersionButton.ToolTip = + Localization.Text("L.Update.CheckFailedTip"); + break; + default: + UpdateStatusText.Text = $"v{current}"; + UpdateVersionButton.ToolTip = _updateCheckInProgress + ? Localization.Text("L.Update.CheckingTip") + : Localization.Format( + "L.Update.UpToDateTip", + current); + break; + } + } + private void ShowSettings() { LoadSettingsControls(); @@ -1261,6 +1490,16 @@ private string LocalizedCaptureSource(string? _) => ? "L.Video.GameLower" : "L.Video.DesktopLower"); + private enum UpdateDisplayState + { + Current, + Checking, + Available, + Downloading, + Installing, + CheckFailed, + } + private sealed record AudioDeviceSelection(bool IsSystem, string Id); private sealed record DeviceListsSnapshot( diff --git a/src/Captail/Themes/Theme.xaml b/src/Captail/Themes/Theme.xaml index 4b35740..3d2378f 100644 --- a/src/Captail/Themes/Theme.xaml +++ b/src/Captail/Themes/Theme.xaml @@ -81,6 +81,8 @@ M9 2h6v12H9z M5 10v1a7 7 0 0 0 14 0v-1 M12 18v4 M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6z M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33 1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82 1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z + M12 2a10 10 0 0 0-3.16 19.49c.5.09.68-.22.68-.48v-1.69c-2.78.6-3.37-1.18-3.37-1.18-.45-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.61.07-.61 1 .07 1.53 1.03 1.53 1.03.9 1.53 2.35 1.09 2.92.83.09-.65.35-1.09.64-1.34-2.22-.25-4.56-1.11-4.56-4.94 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.64 0 0 .84-.27 2.75 1.02A9.58 9.58 0 0 1 12 7.7a9.6 9.6 0 0 1 2.5.34c1.91-1.29 2.75-1.02 2.75-1.02.55 1.37.2 2.39.1 2.64.64.7 1.03 1.59 1.03 2.68 0 3.84-2.34 4.68-4.57 4.93.36.31.68.92.68 1.86V21c0 .27.18.58.69.48A10 10 0 0 0 12 2z + + + + + + + + + + + + +