fix(stream): keep recovery notifications and stop on the dispatcher
The FFmpeg ProcessFailed event arrives off-thread. Start recovery on the UI dispatcher, publish retry status there, and route StopStream back to the dispatcher before changing bound state or alert collections. This prevents a recovery exception from becoming an unhandled WPF CollectionView crash. AttentionAlertStackTests covers worker-thread recovery reporting and stop. 616 tests green, clean build with 0 warnings.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Xunit;
|
||||
using ytLive.Models;
|
||||
@@ -126,6 +127,53 @@ public sealed class AttentionAlertStackTests
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RecoveryReportAndStop_FromWorkerThread_MarshalAlertChangesToDispatcher()
|
||||
{
|
||||
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-alerts-{Guid.NewGuid():N}.db");
|
||||
MainViewModel.LayoutPathOverride = tempDb;
|
||||
MainViewModel? vm = null;
|
||||
try
|
||||
{
|
||||
await _app.RunAsync(async () =>
|
||||
{
|
||||
vm = new MainViewModel();
|
||||
var dispatcher = System.Windows.Application.Current!.Dispatcher;
|
||||
var changedOnUi = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
vm.AttentionAlerts.CollectionChanged += (_, _) =>
|
||||
changedOnUi.TrySetResult(dispatcher.CheckAccess());
|
||||
|
||||
await Task.Run(() => vm.ReportRecoveryStatus(
|
||||
"Stream dropped — reconnecting (attempt 1 of 4)", 10));
|
||||
Assert.True(await changedOnUi.Task.WaitAsync(TimeSpan.FromSeconds(5)));
|
||||
|
||||
var stopped = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
vm.PropertyChanged += (_, args) =>
|
||||
{
|
||||
if (args.PropertyName == nameof(vm.StreamStatus) && vm.IsOffline)
|
||||
stopped.TrySetResult(dispatcher.CheckAccess());
|
||||
};
|
||||
vm.StreamStatus = ytLive.Models.StreamStatus.Streaming;
|
||||
|
||||
await Task.Run(() => vm.EndStreamCommand.Execute(null));
|
||||
Assert.True(await stopped.Task.WaitAsync(TimeSpan.FromSeconds(5)));
|
||||
Assert.DoesNotContain(vm.AttentionAlerts, alert => alert.Id == "stream-recovery");
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (vm != null)
|
||||
await _app.RunAsync(() =>
|
||||
{
|
||||
vm.Shutdown();
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
MainViewModel.LayoutPathOverride = null;
|
||||
SqliteConnection.ClearAllPools();
|
||||
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
|
||||
}
|
||||
}
|
||||
|
||||
private void WithVm(Action<MainViewModel> body) => WithVm((vm, _) => body(vm));
|
||||
|
||||
private void WithVm(Action<MainViewModel, string> body)
|
||||
|
||||
Reference in New Issue
Block a user