From ea212c31b89ff354d8a61fd4fc98ccaf9d211a80 Mon Sep 17 00:00:00 2001 From: Franz Rolfsvaag Date: Fri, 24 Jul 2026 23:39:14 +0200 Subject: [PATCH] fix: surface stream test prerequisites --- CHANGELOG.md | 6 ++ companion/installer/Lumi.Companion.iss | 2 +- companion/scripts/publish-companion.ps1 | 2 +- .../Lumi.Companion.App/CompanionRuntime.cs | 76 +++++++++++++++++-- .../src/Lumi.Companion.App/CompanionState.cs | 5 ++ .../Lumi.Companion.App.csproj | 4 +- .../src/Lumi.Companion.App/MainWindow.axaml | 19 +++++ .../Lumi.Companion.App/MainWindow.axaml.cs | 36 +++++++-- .../Lumi.Companion.App/ObsBridgeManager.cs | 13 +++- knowledge/core/lumi-core.md | 72 +++++++++++++++++- knowledge/plugins/lumi-transcription.md | 2 +- package-lock.json | 4 +- package.json | 2 +- plugins/lumi_transcription/CHANGELOG.md | 6 ++ .../companion_manifest.json | 16 ++-- plugins/lumi_transcription/plugin.json | 2 +- release-index.json | 32 ++++++++ scripts/verify-release-metadata.js | 10 +-- scripts/verify-stream-testing.js | 14 +++- scripts/verify-update-system.js | 9 ++- src/services/stream-testing.js | 31 +++++++- update-manifest.json | 18 ++++- 22 files changed, 330 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01555d0..bcbea8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Lumi changelog +## 0.3.1 + +- Fixed apparently inert Stream Testing starts by surfacing the server rejection immediately in the Stream Testing panel and local diagnostic log. +- Added bounded Windows FFmpeg discovery for verified WinGet and conventional installations, while preserving explicit `LUMI_FFMPEG_PATH` configuration and Linux PATH behavior. +- Added explicit loaded/installed/bundled OBS Bridge version comparison, update-required notices and actions on Overview and Connection & device, and blocked Stream Testing until an outdated bridge is updated and loaded. + ## 0.3.0 - Added admin-only private stream testing for the real OBS output with expiring authenticated sessions, supervised FFmpeg ingest, source/720p/480p no-upscale HLS, automatic/manual quality, audio, fullscreen, reused captions, real OBS/receiver diagnostics, bounded cleanup, and a deterministic test pattern. diff --git a/companion/installer/Lumi.Companion.iss b/companion/installer/Lumi.Companion.iss index a69ba11..aec1f9c 100644 --- a/companion/installer/Lumi.Companion.iss +++ b/companion/installer/Lumi.Companion.iss @@ -1,5 +1,5 @@ #ifndef AppVersion - #define AppVersion "0.2.0" + #define AppVersion "0.2.1" #endif #ifndef SourceRoot #error SourceRoot must point at the self-contained Companion publish directory. diff --git a/companion/scripts/publish-companion.ps1 b/companion/scripts/publish-companion.ps1 index 8e95f66..a11f3ba 100644 --- a/companion/scripts/publish-companion.ps1 +++ b/companion/scripts/publish-companion.ps1 @@ -1,5 +1,5 @@ param( - [string]$Version = "0.2.0", + [string]$Version = "0.2.1", [string]$BridgeVersion = "0.2.0", [string]$ObsVersion = "31.1.1" ) diff --git a/companion/src/Lumi.Companion.App/CompanionRuntime.cs b/companion/src/Lumi.Companion.App/CompanionRuntime.cs index f68222b..facb4bc 100644 --- a/companion/src/Lumi.Companion.App/CompanionRuntime.cs +++ b/companion/src/Lumi.Companion.App/CompanionRuntime.cs @@ -278,6 +278,9 @@ public sealed class CompanionRuntime : IAsyncDisposable if (_streamTestRecovery.Exists) throw new InvalidOperationException("Restore the previous OBS stream service before starting another test."); if (!State.Connected || _socket is null) throw new InvalidOperationException("Reconnect Lumi Companion before starting a private stream test."); if (!State.ObsConnected || _obsBridge is null) throw new InvalidOperationException("Open OBS and wait for the managed integration to connect."); + if (State.ObsBridgeUpdateAvailable) + throw new InvalidOperationException($"Update OBS Bridge before starting Stream Testing. Loaded: {State.ObsBridgeLoadedVersion ?? State.ObsBridgeInstalledVersion ?? "unknown"}; required: {State.ObsBridgeBundledVersion ?? "current bundled version"}."); + if (State.ObsBridgeRepairNeeded) throw new InvalidOperationException("Repair the managed OBS integration before starting Stream Testing."); if (State.ObsStreaming || State.ObsRecording) throw new InvalidOperationException("Stop streaming and recording in OBS before starting a private test."); SetState(State with { StreamTestDetail = "Requesting an expiring private receiver from Lumi…", Health = TrayHealth.Operating }); @@ -291,9 +294,15 @@ public sealed class CompanionRuntime : IAsyncDisposable }, _socket.SessionId, cancellationToken); created = await _streamTestSessionSignal.Task.WaitAsync(TimeSpan.FromSeconds(12), cancellationToken); } - catch + catch (Exception error) { try { await _socket.SendAsync("stream_test_stop", new { reason = "obs_stopped" }, _socket.SessionId, CancellationToken.None); } catch { } + SetState(State with + { + Health = TrayHealth.Degraded, + StreamTestDetail = $"The private test could not start. {Friendly(error)}" + }); + await WriteLogAsync("stream_test_start_failed", error.Message); throw; } finally @@ -334,10 +343,16 @@ public sealed class CompanionRuntime : IAsyncDisposable }); await WriteLogAsync("stream_test_started", "Private stream test started; an encrypted OBS recovery snapshot is active."); } - catch + catch (Exception error) { try { await _socket.SendAsync("stream_test_stop", new { session_id = sessionId, reason = "obs_stopped" }, _socket.SessionId, CancellationToken.None); } catch { } await RestoreObsAfterStreamTestAsync("Private test startup did not complete; restoring OBS."); + SetState(State with + { + Health = TrayHealth.Degraded, + StreamTestDetail = $"The private test could not start. {Friendly(error)}" + }); + await WriteLogAsync("stream_test_start_failed", error.Message); throw; } } @@ -731,11 +746,19 @@ public sealed class CompanionRuntime : IAsyncDisposable if (message.Type == "error") { var serverMessage = message.Payload.TryGetProperty("message", out var value) ? value.GetString() : "Lumi reported an error."; + var streamTestPending = _streamTestSessionSignal is not null; if (_streamTestSessionSignal is not null) _streamTestSessionSignal.TrySetException(new InvalidOperationException(serverMessage ?? "Lumi could not create the private stream test.")); _testFailure?.TrySetResult(serverMessage ?? "Lumi reported an inference error."); _benchmarkLifetime?.Cancel(); - SetState(State with { Health = TrayHealth.Degraded, Detail = serverMessage ?? "Lumi reported an error.", BenchmarkRunning = false, BenchmarkDetail = serverMessage ?? "Lumi reported an inference error." }); + SetState(State with + { + Health = TrayHealth.Degraded, + Detail = serverMessage ?? "Lumi reported an error.", + StreamTestDetail = streamTestPending ? serverMessage ?? "Lumi could not create the private stream test." : State.StreamTestDetail, + BenchmarkRunning = false, + BenchmarkDetail = serverMessage ?? "Lumi reported an inference error." + }); } return Task.CompletedTask; } @@ -749,7 +772,13 @@ public sealed class CompanionRuntime : IAsyncDisposable { var health = connected && State.Connected ? TrayHealth.Ready : State.Health; if (!connected) _bridgeSelectionAttached = null; - SetState(State with { ObsBridgeInstalled = connected || State.ObsBridgeInstalled, ObsConnected = connected, Health = health, Detail = connected ? "OBS and Lumi are connected. Choose a microphone and run a safe test." : State.Detail }); + SetState(WithBridgeState(State with + { + ObsConnected = connected, + ObsBridgeLoadedVersion = connected ? State.ObsBridgeLoadedVersion : null, + Health = health, + Detail = connected ? "OBS and Lumi are connected. Choose a microphone and run a safe test." : State.Detail + })); RefreshPathReadiness(); _ = WriteLogAsync("obs_connection", connected ? "OBS bridge connected." : "OBS bridge disconnected."); if (connected) @@ -765,7 +794,19 @@ public sealed class CompanionRuntime : IAsyncDisposable private async Task OnObsMessageAsync(JsonElement message) { var type = message.GetProperty("type").GetString(); - if (type == "obs_state") + if (type == "hello") + { + var loadedVersion = ReadString(message, "bridge_version"); + var next = WithBridgeState(State with { ObsConnected = true, ObsBridgeLoadedVersion = loadedVersion }); + SetState(next with + { + Detail = next.ObsBridgeUpdateAvailable + ? $"OBS Bridge {loadedVersion ?? "unknown"} is loaded, but Companion requires {next.ObsBridgeBundledVersion ?? "the bundled version"}. Close OBS and update the bridge." + : State.Detail + }); + RefreshPathReadiness(); + } + else if (type == "obs_state") { var streaming = ReadBoolean(message, "streaming"); var recording = ReadBoolean(message, "recording"); @@ -1029,7 +1070,7 @@ public sealed class CompanionRuntime : IAsyncDisposable auto_start = _settings.Current.StartWithObs, bridge_installed = bridge.Valid, bridge_connected = State.ObsConnected, - bridge_version = bridge.Version, + bridge_version = State.ObsBridgeLoadedVersion ?? bridge.InstalledVersion ?? bridge.Version, path_test_valid = pathValid, path_test_at = pathValid ? _settings.Current.PathTestPassedAt?.ToUnixTimeMilliseconds() : null }, _socket.SessionId, cancellationToken); @@ -1042,7 +1083,7 @@ public sealed class CompanionRuntime : IAsyncDisposable string.IsNullOrWhiteSpace(State.Host) || string.IsNullOrWhiteSpace(_settings.Current.PrimarySourceUuid)) return null; if (State.ObsConnected && _bridgeSelectionAttached == false) return null; if (State.ObsConnected && ObsSources.FirstOrDefault(source => source.Uuid == _settings.Current.PrimarySourceUuid) is not { Missing: false, Active: true }) return null; - var value = string.Join("|", PathValidationContract, State.Host, _settings.Current.PrimarySourceUuid, bridge.Version, _serverReadinessFingerprint); + var value = string.Join("|", PathValidationContract, State.Host, _settings.Current.PrimarySourceUuid, State.ObsBridgeLoadedVersion ?? bridge.Version, _serverReadinessFingerprint); return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))).ToLowerInvariant(); } @@ -1106,7 +1147,26 @@ public sealed class CompanionRuntime : IAsyncDisposable } private bool DetectBridgeInstallation() => _bridgeManager.Inspect().Valid; - private CompanionState WithBridgeState(CompanionState state) { var bridge = _bridgeManager.Inspect(); return state with { ObsBridgeInstalled = bridge.Valid, ObsBridgeRepairNeeded = bridge.Installed && !bridge.Valid, ObsBridgePackageAvailable = bridge.PackageAvailable, ObsBridgeDetail = bridge.Detail }; } + private CompanionState WithBridgeState(CompanionState state) + { + var bridge = _bridgeManager.Inspect(); + var loadedUpdate = state.ObsConnected && !string.IsNullOrWhiteSpace(state.ObsBridgeLoadedVersion) && + !string.Equals(state.ObsBridgeLoadedVersion, bridge.Version, StringComparison.OrdinalIgnoreCase); + var updateAvailable = bridge.UpdateAvailable || loadedUpdate; + var detail = loadedUpdate + ? $"OBS is running Bridge {state.ObsBridgeLoadedVersion}; Companion includes {bridge.Version}. Close OBS, choose Update OBS Bridge, then restart OBS." + : bridge.Detail; + return state with + { + ObsBridgeInstalled = bridge.Valid, + ObsBridgeRepairNeeded = bridge.Installed && !bridge.Valid && !bridge.UpdateAvailable, + ObsBridgeUpdateAvailable = updateAvailable, + ObsBridgePackageAvailable = bridge.PackageAvailable, + ObsBridgeInstalledVersion = bridge.InstalledVersion, + ObsBridgeBundledVersion = bridge.Version, + ObsBridgeDetail = detail + }; + } private static string? FindBundledPairingPackage() { diff --git a/companion/src/Lumi.Companion.App/CompanionState.cs b/companion/src/Lumi.Companion.App/CompanionState.cs index 043dd56..96be4fa 100644 --- a/companion/src/Lumi.Companion.App/CompanionState.cs +++ b/companion/src/Lumi.Companion.App/CompanionState.cs @@ -34,7 +34,11 @@ public sealed record CompanionState( bool PathTestValid = false, string PathTestDetail = "Run once after setup or a relevant configuration change.", bool ObsBridgeRepairNeeded = false, + bool ObsBridgeUpdateAvailable = false, bool ObsBridgePackageAvailable = false, + string? ObsBridgeInstalledVersion = null, + string? ObsBridgeLoadedVersion = null, + string? ObsBridgeBundledVersion = null, string ObsBridgeDetail = "Checking the managed OBS integration…") { public string Summary => Health switch @@ -44,6 +48,7 @@ public sealed record CompanionState( TrayHealth.Failed => "Needs attention", _ when !Paired => "Setup required", _ when !Connected => "Lumi offline", + _ when ObsBridgeUpdateAvailable => "OBS update required", _ when !ObsBridgeInstalled => "OBS setup required", _ => "Partially ready" }; diff --git a/companion/src/Lumi.Companion.App/Lumi.Companion.App.csproj b/companion/src/Lumi.Companion.App/Lumi.Companion.App.csproj index 44aed2c..45a8818 100644 --- a/companion/src/Lumi.Companion.App/Lumi.Companion.App.csproj +++ b/companion/src/Lumi.Companion.App/Lumi.Companion.App.csproj @@ -6,8 +6,8 @@ enable app.manifest Assets\Lumi.Companion.ico - 0.2.0 - 0.2.0.0 + 0.2.1 + 0.2.1.0 diff --git a/companion/src/Lumi.Companion.App/MainWindow.axaml b/companion/src/Lumi.Companion.App/MainWindow.axaml index 7e9ecc7..71528b7 100644 --- a/companion/src/Lumi.Companion.App/MainWindow.axaml +++ b/companion/src/Lumi.Companion.App/MainWindow.axaml @@ -82,6 +82,16 @@ + + + + + + +