fix: surface stream test prerequisites
This commit is contained in:
parent
dceebce43a
commit
ea212c31b8
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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"
|
||||
)
|
||||
|
||||
@ -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()
|
||||
{
|
||||
|
||||
@ -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"
|
||||
};
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<ApplicationIcon>Assets\Lumi.Companion.ico</ApplicationIcon>
|
||||
<Version>0.2.0</Version>
|
||||
<AssemblyVersion>0.2.0.0</AssemblyVersion>
|
||||
<Version>0.2.1</Version>
|
||||
<AssemblyVersion>0.2.1.0</AssemblyVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\Lumi.Companion.ico" />
|
||||
|
||||
@ -82,6 +82,16 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="OverviewBridgeUpdatePanel" Background="#FFF2D6" BorderBrush="#D58A1F" BorderThickness="1" CornerRadius="12" Padding="18" IsVisible="False">
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="20">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Update OBS Bridge" FontWeight="Bold" Foreground="#764600" />
|
||||
<TextBlock x:Name="OverviewBridgeUpdateDetail" Text="The OBS integration loaded by OBS is older than the version bundled with Companion." TextWrapping="Wrap" Foreground="#764600" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" x:Name="OverviewUpdateBridgeButton" Classes="primary" Content="Update OBS Bridge" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="Setup progress" Classes="sectionTitle" />
|
||||
<Grid ColumnDefinitions="*,*,*" ColumnSpacing="12">
|
||||
@ -313,6 +323,15 @@
|
||||
<Button x:Name="PairButton" Classes="secondary" Content="Choose pairing package" />
|
||||
<Button x:Name="OpenWebButton" Classes="secondary" Content="Open Lumi WebUI" />
|
||||
</StackPanel>
|
||||
<Border x:Name="ConnectionBridgeUpdatePanel" Background="#FFF2D6" BorderBrush="#D58A1F" BorderThickness="1" CornerRadius="12" Padding="18" IsVisible="False">
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="20">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="OBS Bridge update required" FontWeight="Bold" Foreground="#764600" />
|
||||
<TextBlock x:Name="ConnectionBridgeUpdateDetail" Text="Close OBS before updating the managed integration." TextWrapping="Wrap" Foreground="#764600" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" x:Name="ConnectionUpdateBridgeButton" Classes="primary" Content="Update OBS Bridge" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border Classes="soft">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="Managed OBS integration" FontWeight="SemiBold" />
|
||||
|
||||
@ -105,7 +105,9 @@ public partial class MainWindow : Window
|
||||
SaveSettingsButton.Click += async (_, _) => await SaveSettingsAsync();
|
||||
CheckUpdateButton.Click += async (_, _) => await RunUiActionAsync(() => _runtime.CheckForUpdatesAsync(), CheckUpdateButton);
|
||||
ApplyUpdateButton.Click += async (_, _) => await ApplyUpdateAsync();
|
||||
InstallBridgeButton.Click += async (_, _) => await InstallBridgeAsync();
|
||||
InstallBridgeButton.Click += async (_, _) => await InstallBridgeAsync(InstallBridgeButton);
|
||||
OverviewUpdateBridgeButton.Click += async (_, _) => await InstallBridgeAsync(OverviewUpdateBridgeButton);
|
||||
ConnectionUpdateBridgeButton.Click += async (_, _) => await InstallBridgeAsync(ConnectionUpdateBridgeButton);
|
||||
RemoveBridgeButton.Click += async (_, _) => await RemoveBridgeAsync();
|
||||
AdvancedToggle.IsCheckedChanged += (_, _) => AdvancedPanel.IsVisible = AdvancedToggle.IsChecked == true;
|
||||
SaveSongOverlayButton.Click += async (_, _) => await SaveSongOverlayAsync();
|
||||
@ -149,7 +151,7 @@ public partial class MainWindow : Window
|
||||
{
|
||||
if (!_runtime.State.Paired) { await ChoosePairingPackageAsync(); return; }
|
||||
if (!_runtime.State.Connected) { await RunUiActionAsync(() => _runtime.RetryConnectionAsync(), NextActionButton); return; }
|
||||
if (!_runtime.State.ObsBridgeInstalled) { ShowPage(CompanionPage.Connection); return; }
|
||||
if (_runtime.State.ObsBridgeUpdateAvailable || !_runtime.State.ObsBridgeInstalled) { ShowPage(CompanionPage.Connection); return; }
|
||||
ShowPage(CompanionPage.Test);
|
||||
}
|
||||
|
||||
@ -174,9 +176,12 @@ public partial class MainWindow : Window
|
||||
finally { RenderState(_runtime.State); }
|
||||
}
|
||||
|
||||
private async Task InstallBridgeAsync()
|
||||
private async Task InstallBridgeAsync(Button initiatingButton)
|
||||
{
|
||||
initiatingButton.IsEnabled = false;
|
||||
InstallBridgeButton.IsEnabled = false;
|
||||
OverviewUpdateBridgeButton.IsEnabled = false;
|
||||
ConnectionUpdateBridgeButton.IsEnabled = false;
|
||||
BridgeStatusText.Text = "Checking the packaged integration and requesting Windows approval…";
|
||||
try { await _runtime.InstallOrRepairObsBridgeAsync(); }
|
||||
catch (Exception error)
|
||||
@ -291,8 +296,10 @@ public partial class MainWindow : Window
|
||||
OverviewDetail.Text = state.Detail;
|
||||
PairingStepSymbol.Text = state.Paired ? "✓" : "○";
|
||||
PairingStepDetail.Text = state.Connected ? "Securely connected" : state.Paired ? "Paired, currently offline" : "Not paired";
|
||||
ObsStepSymbol.Text = state.ObsConnected ? "✓" : state.ObsBridgeInstalled ? "◐" : "○";
|
||||
ObsStepDetail.Text = state.ObsConnected ? "Connected to OBS" : state.ObsBridgeInstalled ? "Installed; waiting for OBS" : "Installation required";
|
||||
ObsStepSymbol.Text = state.ObsBridgeUpdateAvailable ? "!" : state.ObsConnected ? "✓" : state.ObsBridgeInstalled ? "◐" : "○";
|
||||
ObsStepDetail.Text = state.ObsBridgeUpdateAvailable
|
||||
? $"Update required ({state.ObsBridgeLoadedVersion ?? state.ObsBridgeInstalledVersion ?? "unknown"} → {state.ObsBridgeBundledVersion ?? "current"})"
|
||||
: state.ObsConnected ? "Connected to OBS" : state.ObsBridgeInstalled ? "Installed; waiting for OBS" : "Installation required";
|
||||
TestStepSymbol.Text = state.PathTestValid ? "✓" : "○";
|
||||
TestStepDetail.Text = state.PathTestDetail;
|
||||
DeviceNameText.Text = state.DeviceName ?? "Not paired";
|
||||
@ -311,7 +318,7 @@ public partial class MainWindow : Window
|
||||
StreamTestBitrateText.Text = state.StreamTestRunning ? $"{state.StreamTestBitrateKbps:0} kbps" : "—";
|
||||
StreamTestDroppedText.Text = state.StreamTestRunning ? $"{state.StreamTestDroppedFrames} / {state.StreamTestTotalFrames}" : "—";
|
||||
StreamTestCongestionText.Text = state.StreamTestRunning ? $"{state.StreamTestCongestion:P0}" : "—";
|
||||
StartStreamTestButton.IsEnabled = state.Connected && state.ObsConnected && !state.ObsStreaming && !state.ObsRecording && !state.StreamTestRunning && !state.StreamTestRecoveryRequired;
|
||||
StartStreamTestButton.IsEnabled = state.Connected && state.ObsConnected && !state.ObsBridgeUpdateAvailable && !state.ObsBridgeRepairNeeded && !state.ObsStreaming && !state.ObsRecording && !state.StreamTestRunning && !state.StreamTestRecoveryRequired;
|
||||
StopStreamTestButton.IsEnabled = state.StreamTestRunning;
|
||||
RepairStreamTestButton.IsVisible = state.StreamTestRecoveryRequired && !state.StreamTestRunning;
|
||||
UpdatePanel.IsVisible = state.UpdateAvailable;
|
||||
@ -323,10 +330,17 @@ public partial class MainWindow : Window
|
||||
CurrentVersionText.Text = CompanionRuntime.DisplayVersion;
|
||||
UpdateStatusText.Text = state.UpdateDetail;
|
||||
BridgeStatusText.Text = state.ObsBridgeDetail;
|
||||
InstallBridgeButton.Content = state.ObsBridgeRepairNeeded ? "Repair integration" : state.ObsBridgeInstalled ? "Reinstall integration" : "Install integration";
|
||||
OverviewBridgeUpdatePanel.IsVisible = state.ObsBridgeUpdateAvailable;
|
||||
ConnectionBridgeUpdatePanel.IsVisible = state.ObsBridgeUpdateAvailable;
|
||||
OverviewBridgeUpdateDetail.Text = state.ObsBridgeDetail;
|
||||
ConnectionBridgeUpdateDetail.Text = state.ObsBridgeDetail;
|
||||
InstallBridgeButton.Content = state.ObsBridgeUpdateAvailable ? "Update OBS Bridge" : state.ObsBridgeRepairNeeded ? "Repair integration" : state.ObsBridgeInstalled ? "Reinstall integration" : "Install integration";
|
||||
// The maintenance action performs a fresh package/process check and reports the
|
||||
// actual blocker, so a stale OBS connection signal must never strand repair.
|
||||
InstallBridgeButton.IsEnabled = !state.TestRunning && !state.BenchmarkRunning;
|
||||
var bridgeMaintenanceEnabled = !state.TestRunning && !state.BenchmarkRunning && !state.StreamTestRunning;
|
||||
InstallBridgeButton.IsEnabled = bridgeMaintenanceEnabled;
|
||||
OverviewUpdateBridgeButton.IsEnabled = bridgeMaintenanceEnabled;
|
||||
ConnectionUpdateBridgeButton.IsEnabled = bridgeMaintenanceEnabled;
|
||||
RemoveBridgeButton.IsEnabled = state.ObsBridgeInstalled || state.ObsBridgeRepairNeeded;
|
||||
|
||||
if (!state.Paired)
|
||||
@ -341,6 +355,12 @@ public partial class MainWindow : Window
|
||||
NextActionDetail.Text = "Your device is paired, but Lumi cannot currently be reached.";
|
||||
NextActionButton.Content = "Retry connection";
|
||||
}
|
||||
else if (state.ObsBridgeUpdateAvailable)
|
||||
{
|
||||
NextActionTitle.Text = "Update OBS Bridge";
|
||||
NextActionDetail.Text = state.ObsBridgeDetail;
|
||||
NextActionButton.Content = "Manage OBS integration";
|
||||
}
|
||||
else if (!state.ObsBridgeInstalled)
|
||||
{
|
||||
NextActionTitle.Text = "Install the OBS integration";
|
||||
|
||||
@ -23,9 +23,11 @@ public sealed class ObsBridgeManager
|
||||
var packageAvailable = package is not null;
|
||||
var fileInstalled = File.Exists(InstalledDll);
|
||||
var valid = packageAvailable && fileInstalled && HashFile(InstalledDll) == package!.Manifest.Sha256 && installed?.Version == package.Manifest.Version;
|
||||
return new ObsBridgeStatus(packageAvailable, fileInstalled, valid, package?.Manifest.Version,
|
||||
var updateAvailable = packageAvailable && fileInstalled && installed is not null && installed.Version != package!.Manifest.Version;
|
||||
return new ObsBridgeStatus(packageAvailable, fileInstalled, valid, updateAvailable, installed?.Version, package?.Manifest.Version,
|
||||
valid ? $"OBS integration {package!.Manifest.Version} is installed. Restart OBS if it was open during the last repair." :
|
||||
!packageAvailable ? $"The bundled OBS integration could not be verified. {_packageFailure ?? "The packaged component was not found."}" :
|
||||
updateAvailable ? $"OBS Bridge {installed!.Version} is installed; Companion includes {package!.Manifest.Version}. Close OBS, then update the bridge." :
|
||||
fileInstalled ? "The OBS integration is outdated or damaged. Repair it while OBS is closed." : "The OBS integration is ready to install.");
|
||||
}
|
||||
|
||||
@ -242,5 +244,12 @@ public sealed record ObsBridgeManifest(
|
||||
[property: JsonPropertyName("version")] string Version,
|
||||
[property: JsonPropertyName("sha256")] string Sha256,
|
||||
[property: JsonPropertyName("obs_minimum_version")] string ObsMinimumVersion);
|
||||
public sealed record ObsBridgeStatus(bool PackageAvailable, bool Installed, bool Valid, string? Version, string Detail);
|
||||
public sealed record ObsBridgeStatus(
|
||||
bool PackageAvailable,
|
||||
bool Installed,
|
||||
bool Valid,
|
||||
bool UpdateAvailable,
|
||||
string? InstalledVersion,
|
||||
string? Version,
|
||||
string Detail);
|
||||
internal sealed record ObsBridgePackage(ObsBridgeManifest Manifest, byte[] Dll, byte[]? Locale);
|
||||
|
||||
@ -14,7 +14,7 @@ editable: false
|
||||
Lumi is the core web UI and bot runtime.
|
||||
## Runtime
|
||||
Package: lumi-bot
|
||||
Version: 0.3.0
|
||||
Version: 0.3.1
|
||||
## Routes
|
||||
- POST /api/diagnostics/v1/run
|
||||
- GET /api/events
|
||||
@ -93,6 +93,13 @@ Version: 0.3.0
|
||||
- POST /admin/theming/custom/:id/delete
|
||||
- POST /admin/theming
|
||||
- GET /admin/diagnostics
|
||||
- GET /admin/stream-testing
|
||||
- GET /admin/stream-testing/status
|
||||
- POST /admin/stream-testing/stop
|
||||
- POST /admin/stream-testing/pattern/start
|
||||
- GET /admin/stream-testing/media/:id/captions.vtt
|
||||
- GET /admin/stream-testing/media/:id/:name
|
||||
- GET /admin/stream-testing/hls.js
|
||||
- POST /admin/diagnostics/run
|
||||
- POST /admin/diagnostics/access/renew
|
||||
- POST /admin/diagnostics/access/revoke
|
||||
@ -908,6 +915,69 @@ Version: 0.3.0
|
||||
- Side effects: Usually read-only.
|
||||
- Limits/notes: Generated from static route source analysis; confirm exact behavior in the handler before changing integrations.
|
||||
|
||||
### GET /admin/stream-testing
|
||||
|
||||
- Purpose: Renders the admin stream testing WebUI page.
|
||||
- Inputs: No request parameters detected by static analysis.
|
||||
- Response format: HTML page rendered from an EJS view
|
||||
- Access: admin access expected
|
||||
- Side effects: Usually read-only.
|
||||
- Limits/notes: Generated from static route source analysis; confirm exact behavior in the handler before changing integrations.
|
||||
|
||||
### GET /admin/stream-testing/status
|
||||
|
||||
- Purpose: Provides admin stream testing status data as JSON.
|
||||
- Inputs: No request parameters detected by static analysis.
|
||||
- Response format: JSON response
|
||||
- Access: admin access expected
|
||||
- Side effects: Usually read-only.
|
||||
- Limits/notes: Generated from static route source analysis; confirm exact behavior in the handler before changing integrations.
|
||||
|
||||
### POST /admin/stream-testing/stop
|
||||
|
||||
- Purpose: Provides admin stream testing stop data as JSON.
|
||||
- Inputs: body: full submitted body is passed to a helper; exact fields are defined by the matching form/service
|
||||
- Response format: JSON response
|
||||
- Access: admin access expected
|
||||
- Side effects: Action route; side effects were not detected statically.
|
||||
- Limits/notes: Generated from static route source analysis; confirm exact behavior in the handler before changing integrations. Errors are caught and usually returned as a flash message, JSON error, or error page. Most non-API POST routes are browser form submissions and usually redirect after completion.
|
||||
|
||||
### POST /admin/stream-testing/pattern/start
|
||||
|
||||
- Purpose: Processes the admin stream testing pattern start action and stores or applies submitted form data.
|
||||
- Inputs: No request parameters detected by static analysis.
|
||||
- Response format: Form/action response; exact format was not detected statically.
|
||||
- Access: admin access expected
|
||||
- Side effects: writes or mutates server-side state
|
||||
- Limits/notes: Generated from static route source analysis; confirm exact behavior in the handler before changing integrations. Errors are caught and usually returned as a flash message, JSON error, or error page. Most non-API POST routes are browser form submissions and usually redirect after completion.
|
||||
|
||||
### GET /admin/stream-testing/media/:id/captions.vtt
|
||||
|
||||
- Purpose: Handles admin stream testing media id captions vtt.
|
||||
- Inputs: path params: `id`
|
||||
- Response format: HTML or data response; exact format was not detected statically.
|
||||
- Access: admin access expected
|
||||
- Side effects: Usually read-only.
|
||||
- Limits/notes: Generated from static route source analysis; confirm exact behavior in the handler before changing integrations.
|
||||
|
||||
### GET /admin/stream-testing/media/:id/:name
|
||||
|
||||
- Purpose: Handles admin stream testing media id name.
|
||||
- Inputs: path params: `id`, `name`
|
||||
- Response format: static file response
|
||||
- Access: admin access expected
|
||||
- Side effects: Usually read-only.
|
||||
- Limits/notes: Generated from static route source analysis; confirm exact behavior in the handler before changing integrations.
|
||||
|
||||
### GET /admin/stream-testing/hls.js
|
||||
|
||||
- Purpose: Handles admin stream testing hls js.
|
||||
- Inputs: No request parameters detected by static analysis.
|
||||
- Response format: static file response
|
||||
- Access: admin access expected
|
||||
- Side effects: Usually read-only.
|
||||
- Limits/notes: Generated from static route source analysis; confirm exact behavior in the handler before changing integrations.
|
||||
|
||||
### POST /admin/diagnostics/run
|
||||
|
||||
- Purpose: Processes the admin diagnostics run action and stores or applies submitted form data.
|
||||
|
||||
@ -14,7 +14,7 @@ editable: false
|
||||
Server-hosted whisper.cpp transcription for Lumi Companion and OBS closed captions.
|
||||
## Metadata
|
||||
Plugin ID: lumi_transcription
|
||||
Version: 0.2.0
|
||||
Version: 0.2.1
|
||||
Default state: enabled
|
||||
## Web Routes
|
||||
- /plugins/lumi_transcription
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "lumi-bot",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "lumi-bot",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.6.0",
|
||||
"better-sqlite3": "^11.5.0",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lumi-bot",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
# Lumi Transcription changelog
|
||||
|
||||
## 0.2.1
|
||||
|
||||
- Surface Stream Testing startup failures immediately instead of leaving the Companion action waiting with a stale status.
|
||||
- Detect the OBS Bridge version actually loaded by OBS and show Update OBS Bridge actions on Overview and Connection & device when it differs from the bundled integration.
|
||||
- Prevent private output redirection until an outdated or damaged OBS integration is updated or repaired.
|
||||
|
||||
## 0.2.0
|
||||
|
||||
- Added authenticated private stream-test session control over the existing Companion transport, including reused live caption cues and OBS output diagnostics.
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"signed": false,
|
||||
"release_notes": "Adds private OBS stream testing with encrypted exact-service recovery, live output diagnostics, and repeatable update checks.",
|
||||
"release_notes": "Fixes Stream Testing startup feedback, discovers local FFmpeg installations, and adds explicit outdated OBS Bridge update actions.",
|
||||
"installer": {
|
||||
"id": "windows-x64-installer",
|
||||
"platform": "win32",
|
||||
"architecture": "x64",
|
||||
"label": "Windows x64 per-user installer",
|
||||
"filename": "Lumi.Companion-Setup.exe",
|
||||
"url": "https://git.rolfsvaag.no/Rolfsvaag_Datateknikk/Lumi/releases/download/companion-v0.2.0/Lumi.Companion-Setup.exe",
|
||||
"sha256": "cdeb43dc1512f537ff4cf8840fb36ee7888c1a92d901a50984079beabba221da",
|
||||
"bytes": 51882646
|
||||
"url": "https://git.rolfsvaag.no/Rolfsvaag_Datateknikk/Lumi/releases/download/companion-v0.2.1/Lumi.Companion-Setup.exe",
|
||||
"sha256": "9bf4d9308c31fe9933878ed784350236b0fd4098cbe838dc2dcb8387922c40cd",
|
||||
"bytes": 51883242
|
||||
},
|
||||
"artifacts": [
|
||||
{
|
||||
@ -20,9 +20,9 @@
|
||||
"architecture": "x64",
|
||||
"label": "Windows x64 self-contained",
|
||||
"filename": "Lumi.Companion-win-x64.zip",
|
||||
"url": "https://git.rolfsvaag.no/Rolfsvaag_Datateknikk/Lumi/releases/download/companion-v0.2.0/Lumi.Companion-win-x64.zip",
|
||||
"sha256": "bf42a9abbf1df9eb604f3d7f0216939608af071fecb8b17e3def55037ce2e77f",
|
||||
"bytes": 66775103,
|
||||
"url": "https://git.rolfsvaag.no/Rolfsvaag_Datateknikk/Lumi/releases/download/companion-v0.2.1/Lumi.Companion-win-x64.zip",
|
||||
"sha256": "32ce845e7576f8cd65352df561998f60c00dfb6507ab96089790194446b799e6",
|
||||
"bytes": 66773784,
|
||||
"entrypoint": "Lumi.Companion.App.exe"
|
||||
}
|
||||
]
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "lumi_transcription",
|
||||
"name": "Lumi Transcription",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"description": "Server-hosted whisper.cpp transcription for Lumi Companion and OBS closed captions.",
|
||||
"main": "index.js",
|
||||
"channel": "stable",
|
||||
|
||||
@ -2,6 +2,38 @@
|
||||
"schema_version": 1,
|
||||
"channel": "stable",
|
||||
"releases": [
|
||||
{
|
||||
"version": "0.3.1",
|
||||
"ref": "refs/tags/v0.3.1",
|
||||
"released_at": "2026-07-24",
|
||||
"installable": true,
|
||||
"rollback_safe": true,
|
||||
"replaces_versions": [
|
||||
"1.2.0"
|
||||
],
|
||||
"data_policy": "preserve",
|
||||
"dependency_policy": "sync_on_restart",
|
||||
"migration_notes": "Fixes Stream Testing startup feedback and Windows FFmpeg discovery, and adds explicit loaded OBS Bridge version checks with update actions. Existing local and plugin data remains preserved.",
|
||||
"plugins": {
|
||||
"auto-vc": "0.1.6",
|
||||
"birthday": "0.1.3",
|
||||
"economy-framework": "0.2.10",
|
||||
"economy-games": "0.1.7",
|
||||
"expression-interaction": "0.2.1",
|
||||
"lumi_ai": "0.8.5",
|
||||
"lumi_transcription": "0.2.1",
|
||||
"moderation": "0.1.5",
|
||||
"now_playing": "0.1.2",
|
||||
"okf": "0.1.2",
|
||||
"quotes": "0.1.2",
|
||||
"sample-plugin": "0.1.0",
|
||||
"throne_wishlist": "0.1.2",
|
||||
"welcome_messages": "0.1.1"
|
||||
},
|
||||
"tools": {
|
||||
"lumi_ai_web_search": "0.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.3.0",
|
||||
"ref": "refs/tags/v0.3.0",
|
||||
|
||||
@ -4,12 +4,12 @@ const path = require("path");
|
||||
const { findSafeTarget } = require("../src/services/versioning");
|
||||
|
||||
const root = path.join(__dirname, "..");
|
||||
const releaseVersion = "0.3.0";
|
||||
const previousStableVersion = "0.2.27";
|
||||
const priorStableVersion = "0.2.26";
|
||||
const releaseVersion = "0.3.1";
|
||||
const previousStableVersion = "0.3.0";
|
||||
const priorStableVersion = "0.2.27";
|
||||
const earliestCompatibleCoreVersion = "0.1.9";
|
||||
const introducedPlugins = {
|
||||
lumi_transcription: { version: "0.2.0", knowledge: "lumi-transcription" },
|
||||
lumi_transcription: { version: "0.2.1", knowledge: "lumi-transcription" },
|
||||
now_playing: { version: "0.1.2", knowledge: "now-playing" }
|
||||
};
|
||||
|
||||
@ -82,4 +82,4 @@ assert.equal(webSearch.minimum_lumi_version, "0.2.0");
|
||||
assert.equal(webSearch.minimum_lumi_ai_version, "0.8.2");
|
||||
assert.equal(hasVersionHeading(readText("plugins/lumi_ai_web_search/CHANGELOG.md"), webSearch.version), true);
|
||||
|
||||
console.log("Release metadata verification passed: stable core 0.3.0 after 0.2.27 with synchronized Companion plugin metadata.");
|
||||
console.log("Release metadata verification passed: stable core 0.3.1 after 0.3.0 with synchronized Companion plugin metadata.");
|
||||
|
||||
@ -3,10 +3,11 @@ const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const root = path.join(__dirname, "..");
|
||||
const { ladderFor, ffmpegArgs, streamTestingService } = require("../src/services/stream-testing");
|
||||
const { ladderFor, ffmpegArgs, streamTestingService, resolveFfmpegExecutable } = require("../src/services/stream-testing");
|
||||
const protocol = require("../plugins/lumi_transcription/backend/companion/protocol");
|
||||
|
||||
async function main() {
|
||||
assert.equal(typeof resolveFfmpegExecutable(), "string");
|
||||
const fullHd = ladderFor({ width: 1920, height: 1080, fps: 60 });
|
||||
assert.deepStrictEqual(fullHd.variants.map((item) => item.name), ["source", "720p", "480p"]);
|
||||
assert(fullHd.variants.every((item) => item.width <= 1920 && item.height <= 1080), "adaptive ladder must never upscale");
|
||||
@ -38,6 +39,8 @@ async function main() {
|
||||
const nativeBridge = fs.readFileSync(path.join(root, "companion/native/obs-bridge/src/plugin.cpp"), "utf8");
|
||||
const companion = fs.readFileSync(path.join(root, "companion/src/Lumi.Companion.App/CompanionRuntime.cs"), "utf8");
|
||||
const companionUi = fs.readFileSync(path.join(root, "companion/src/Lumi.Companion.App/MainWindow.axaml.cs"), "utf8");
|
||||
const companionView = fs.readFileSync(path.join(root, "companion/src/Lumi.Companion.App/MainWindow.axaml"), "utf8");
|
||||
const bridgeManager = fs.readFileSync(path.join(root, "companion/src/Lumi.Companion.App/ObsBridgeManager.cs"), "utf8");
|
||||
const webUi = fs.readFileSync(path.join(root, "src/web/views/admin-stream-testing.ejs"), "utf8");
|
||||
|
||||
assert.match(server, /admin\/stream-testing", requireRole\("admin"\)/);
|
||||
@ -49,6 +52,7 @@ async function main() {
|
||||
assert.match(service, /caption_delay/);
|
||||
assert.match(service, /receiver_slow/);
|
||||
assert.match(service, /PUBLIC_INGEST_PORT/);
|
||||
assert.match(service, /Microsoft", "WinGet", "Packages/);
|
||||
assert.strictEqual(/^(?:master|stream_(?:source|720p|480p))(?:\.m3u8|_\d{6}\.ts)$/.test("master.m3u8"), true);
|
||||
assert.match(service, /spawn\(FFMPEG, args, \{[^}]*stdio:/s);
|
||||
assert.match(service, /testsrc2=size=1280x720:rate=30/);
|
||||
@ -63,8 +67,16 @@ async function main() {
|
||||
assert.match(companion, /StartStreamTestAsync/);
|
||||
assert.match(companion, /RestoreObsAfterStreamTestAsync/);
|
||||
assert.match(companion, /finally\s*\{\s*Interlocked\.Exchange\(ref _streamTestRestoreRunning, 0\)/s);
|
||||
assert.match(companion, /StreamTestDetail = streamTestPending/);
|
||||
assert.match(companion, /stream_test_start_failed/);
|
||||
assert.match(companionUi, /CheckUpdateButton\.IsEnabled = !state\.UpdateCheckRunning/);
|
||||
assert.match(companionUi, /CheckUpdateButton\.Content = state\.UpdateCheckRunning \? "Checking…" : "Check now"/);
|
||||
assert.match(companionUi, /ObsBridgeUpdateAvailable/);
|
||||
assert.match(companionUi, /Update OBS Bridge/);
|
||||
assert.match(companionView, /OverviewBridgeUpdatePanel/);
|
||||
assert.match(companionView, /ConnectionBridgeUpdatePanel/);
|
||||
assert.match(bridgeManager, /InstalledVersion/);
|
||||
assert.match(bridgeManager, /UpdateAvailable/);
|
||||
assert.match(webUi, /data-stream-player/);
|
||||
assert.match(webUi, /data-stream-quality/);
|
||||
assert.match(webUi, /data-stream-captions/);
|
||||
|
||||
@ -24,7 +24,7 @@ function readJson(relativePath) {
|
||||
|
||||
const releaseIndex = readJson("release-index.json");
|
||||
const releaseVersions = releaseIndex.releases.map((release) => release.version);
|
||||
assert.deepEqual(releaseVersions, ["0.3.0", "0.2.27", "0.2.26", "0.2.25", "0.2.24", "0.2.23", "0.2.22", "0.2.21", "0.2.20", "0.2.19", "0.2.18", "0.2.17", "0.2.16", "0.2.15", "0.2.14", "0.2.13", "0.2.12", "0.2.11", "0.2.10", "0.2.9", "0.2.8", "0.2.7", "0.2.6", "0.2.5", "0.2.4", "0.2.3", "0.2.2", "0.2.1", "0.2.0", "0.1.9"]);
|
||||
assert.deepEqual(releaseVersions, ["0.3.1", "0.3.0", "0.2.27", "0.2.26", "0.2.25", "0.2.24", "0.2.23", "0.2.22", "0.2.21", "0.2.20", "0.2.19", "0.2.18", "0.2.17", "0.2.16", "0.2.15", "0.2.14", "0.2.13", "0.2.12", "0.2.11", "0.2.10", "0.2.9", "0.2.8", "0.2.7", "0.2.6", "0.2.5", "0.2.4", "0.2.3", "0.2.2", "0.2.1", "0.2.0", "0.1.9"]);
|
||||
assert.equal(new Set(releaseVersions).size, releaseVersions.length, "release versions must be unique");
|
||||
for (const release of releaseIndex.releases) {
|
||||
assert.equal(normalizeRepositoryRef(release.ref), release.ref);
|
||||
@ -37,8 +37,8 @@ const packageVersion = readJson("package.json").version;
|
||||
const coreManifest = readJson("update-manifest.json");
|
||||
assert.equal(packageVersion, coreManifest.version);
|
||||
assert.equal(coreManifest.channel, "stable");
|
||||
assert.equal(packageVersion, "0.3.0");
|
||||
assert.equal(currentRelease.version, "0.3.0");
|
||||
assert.equal(packageVersion, "0.3.1");
|
||||
assert.equal(currentRelease.version, "0.3.1");
|
||||
assert.deepEqual(currentRelease.replaces_versions, ["1.2.0"]);
|
||||
for (const [pluginId, version] of Object.entries(currentRelease.plugins)) {
|
||||
assert.equal(readJson(`plugins/${pluginId}/plugin.json`).version, version, `${pluginId} release catalog version`);
|
||||
@ -50,6 +50,7 @@ for (const [toolId, version] of Object.entries(currentRelease.tools)) {
|
||||
const baseTarget = {
|
||||
current_version: "0.2.4",
|
||||
available_versions: [
|
||||
{ version: "0.3.1", ref: "refs/tags/v0.3.1", rollback_safe: true },
|
||||
{ version: "0.3.0", ref: "refs/tags/v0.3.0", rollback_safe: true },
|
||||
{ version: "0.2.27", ref: "refs/tags/v0.2.27", rollback_safe: true },
|
||||
{ version: "0.2.26", ref: "refs/tags/v0.2.26", rollback_safe: true },
|
||||
@ -149,7 +150,7 @@ const corrected = buildStatus({
|
||||
channel: "stable"
|
||||
});
|
||||
assert.equal(corrected.version_correction, true);
|
||||
assert.equal(corrected.safe_target_version, "0.3.0");
|
||||
assert.equal(corrected.safe_target_version, "0.3.1");
|
||||
assert.equal(corrected.update_available, true);
|
||||
assert.equal(corrected.blocked, false);
|
||||
|
||||
|
||||
@ -13,7 +13,34 @@ const INACTIVITY_MS = Math.min(5 * 60 * 1000, Math.max(20 * 1000, Number(process
|
||||
const MAX_OUTPUT_BYTES = Math.min(4 * 1024 ** 3, Math.max(128 * 1024 ** 2, Number(process.env.LUMI_STREAM_TEST_MAX_BYTES) || 1024 ** 3));
|
||||
const INGEST_PORT = Math.min(65535, Math.max(1024, Number(process.env.LUMI_STREAM_TEST_INGEST_PORT) || 19350));
|
||||
const PUBLIC_INGEST_PORT = Math.min(65535, Math.max(1, Number(process.env.LUMI_STREAM_TEST_PUBLIC_PORT) || INGEST_PORT));
|
||||
const FFMPEG = String(process.env.LUMI_FFMPEG_PATH || "ffmpeg");
|
||||
|
||||
function resolveFfmpegExecutable() {
|
||||
if (process.env.LUMI_FFMPEG_PATH) return String(process.env.LUMI_FFMPEG_PATH);
|
||||
if (process.platform !== "win32") return "ffmpeg";
|
||||
const local = process.env.LOCALAPPDATA;
|
||||
const programFiles = [process.env.ProgramFiles, process.env["ProgramFiles(x86)"]].filter(Boolean);
|
||||
const direct = [
|
||||
local && path.join(local, "Microsoft", "WinGet", "Links", "ffmpeg.exe"),
|
||||
local && path.join(local, "Programs", "ffmpeg", "bin", "ffmpeg.exe"),
|
||||
...programFiles.map((root) => path.join(root, "ffmpeg", "bin", "ffmpeg.exe"))
|
||||
].filter(Boolean);
|
||||
for (const candidate of direct) {
|
||||
try { if (fs.statSync(candidate).isFile()) return candidate; } catch {}
|
||||
}
|
||||
const packages = local && path.join(local, "Microsoft", "WinGet", "Packages");
|
||||
try {
|
||||
for (const packageName of fs.readdirSync(packages).filter((name) => /ffmpeg/i.test(name)).slice(0, 32)) {
|
||||
const packageRoot = path.join(packages, packageName);
|
||||
for (const versionName of fs.readdirSync(packageRoot).slice(0, 32)) {
|
||||
const candidate = path.join(packageRoot, versionName, "bin", "ffmpeg.exe");
|
||||
try { if (fs.statSync(candidate).isFile()) return candidate; } catch {}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return "ffmpeg";
|
||||
}
|
||||
|
||||
const FFMPEG = resolveFfmpegExecutable();
|
||||
|
||||
function finite(value, fallback, min, max) {
|
||||
const parsed = Number(value);
|
||||
@ -502,4 +529,4 @@ class StreamTestingService {
|
||||
|
||||
const streamTestingService = new StreamTestingService();
|
||||
|
||||
module.exports = { StreamTestingService, streamTestingService, ladderFor, executableStatus, ffmpegArgs, DATA_ROOT };
|
||||
module.exports = { StreamTestingService, streamTestingService, ladderFor, executableStatus, ffmpegArgs, resolveFfmpegExecutable, DATA_ROOT };
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "Lumi Core",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"channel": "stable",
|
||||
"released_at": "2026-07-24",
|
||||
"compatible_from": "0.1.9",
|
||||
"migration_kind": "minor",
|
||||
"migration_kind": "patch",
|
||||
"replaces_versions": [
|
||||
"1.2.0"
|
||||
],
|
||||
"migration_notes": "Adds private OBS stream testing, crash-safe exact-service restoration, corrected overlay media boundaries, and repeatable Companion update checks. Existing settings, databases, pairing records, stream credentials, overlays, uploads, models, secrets, plugin data, and local-only plugins are preserved. Stream-test fragments are ephemeral and removed at session end.",
|
||||
"migration_notes": "Fixes Stream Testing startup feedback and Windows FFmpeg discovery, and adds explicit outdated OBS Bridge update guidance and actions. Existing settings, databases, pairing records, OBS settings, credentials, overlays, uploads, models, secrets, plugin data, and local-only plugins are preserved.",
|
||||
"rollback_safe": true,
|
||||
"requirements": [
|
||||
"Node.js 18 or newer",
|
||||
@ -350,6 +350,18 @@
|
||||
],
|
||||
"rollback_safe": true,
|
||||
"migration_notes": "Completes exact-release bundled-plugin synchronization after legacy core-only updates while preserving local data."
|
||||
},
|
||||
{
|
||||
"version": "0.3.0",
|
||||
"channel": "stable",
|
||||
"released_at": "2026-07-24",
|
||||
"compatible_from": "0.1.9",
|
||||
"migration_kind": "minor",
|
||||
"replaces_versions": [
|
||||
"1.2.0"
|
||||
],
|
||||
"rollback_safe": true,
|
||||
"migration_notes": "Adds private OBS stream testing, crash-safe exact-service restoration, corrected overlay media boundaries, and repeatable Companion update checks while preserving local data."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user