diff --git a/src/OpenIPC.Viewer.App/Services/CameraEditorFactory.cs b/src/OpenIPC.Viewer.App/Services/CameraEditorFactory.cs index d368a8a..868512f 100644 --- a/src/OpenIPC.Viewer.App/Services/CameraEditorFactory.cs +++ b/src/OpenIPC.Viewer.App/Services/CameraEditorFactory.cs @@ -10,18 +10,20 @@ public sealed class CameraEditorFactory { private readonly IVideoEngine _engine; private readonly CameraDirectoryService _directory; + private readonly UserSettingsService _userSettings; private readonly ILoggerFactory _loggerFactory; - public CameraEditorFactory(IVideoEngine engine, CameraDirectoryService directory, ILoggerFactory loggerFactory) + public CameraEditorFactory(IVideoEngine engine, CameraDirectoryService directory, UserSettingsService userSettings, ILoggerFactory loggerFactory) { _engine = engine; _directory = directory; + _userSettings = userSettings; _loggerFactory = loggerFactory; } public CameraEditorViewModel CreateForNew() => - new(_engine, _directory, _loggerFactory.CreateLogger()); + new(_engine, _directory, _userSettings, _loggerFactory.CreateLogger()); public CameraEditorViewModel CreateForEdit(Camera existing, CameraCredentials? credentials) => - new(existing, credentials, _engine, _directory, _loggerFactory.CreateLogger()); + new(existing, credentials, _engine, _directory, _userSettings, _loggerFactory.CreateLogger()); } diff --git a/src/OpenIPC.Viewer.App/ViewModels/CameraLibraryPageViewModel.cs b/src/OpenIPC.Viewer.App/ViewModels/CameraLibraryPageViewModel.cs index 89a7242..f142ada 100644 --- a/src/OpenIPC.Viewer.App/ViewModels/CameraLibraryPageViewModel.cs +++ b/src/OpenIPC.Viewer.App/ViewModels/CameraLibraryPageViewModel.cs @@ -209,6 +209,14 @@ private void RefilterCameras() _ = ProbeReachabilityAsync(); } + /// + /// Re-runs reachability probes for the rows already on screen. Called by + /// the view on every Loaded — the full LoadAsync only runs once (IsLoaded + /// gate), so without this a status probed before e.g. a Wi-Fi hiccup + /// stayed OFFLINE forever while the stream itself played fine. + /// + public Task ReprobeReachabilityAsync() => ProbeReachabilityAsync(); + private async Task ProbeReachabilityAsync() { var rows = new System.Collections.Generic.List(Cameras); @@ -476,10 +484,16 @@ public async Task RefreshReachabilityAsync(CancellationToken ct) var port = Camera.RtspMainUri.Port; if (port <= 0) port = 554; + // Probe the endpoint the player actually dials. The RTSP URI host can + // differ from the Host field (ONVIF behind NAT, mDNS name vs IP) — a + // probe against the wrong one showed OFFLINE while the stream played. + var host = Camera.RtspMainUri.Host; + if (string.IsNullOrEmpty(host)) host = Camera.Host; + try { var reachable = await _reachability - .IsReachableAsync(Camera.Host, port, ProbeTimeout, ct) + .IsReachableAsync(host, port, ProbeTimeout, ct) .ConfigureAwait(true); Status = reachable ? CameraReachability.Online : CameraReachability.Offline; } diff --git a/src/OpenIPC.Viewer.App/ViewModels/Dialogs/CameraEditorViewModel.cs b/src/OpenIPC.Viewer.App/ViewModels/Dialogs/CameraEditorViewModel.cs index e29ced5..394d983 100644 --- a/src/OpenIPC.Viewer.App/ViewModels/Dialogs/CameraEditorViewModel.cs +++ b/src/OpenIPC.Viewer.App/ViewModels/Dialogs/CameraEditorViewModel.cs @@ -18,6 +18,7 @@ public sealed partial class CameraEditorViewModel : ViewModelBase { private readonly IVideoEngine? _engine; private readonly CameraDirectoryService? _directory; + private readonly UserSettingsService? _userSettings; private readonly ILogger? _logger; private GroupId? _pendingGroupId; @@ -48,15 +49,16 @@ public sealed partial class CameraEditorViewModel : ViewModelBase public CameraEditorViewModel() { } - public CameraEditorViewModel(IVideoEngine engine, CameraDirectoryService directory, ILogger logger) + public CameraEditorViewModel(IVideoEngine engine, CameraDirectoryService directory, UserSettingsService userSettings, ILogger logger) { _engine = engine; _directory = directory; + _userSettings = userSettings; _logger = logger; } - public CameraEditorViewModel(Camera existing, CameraCredentials? credentials, IVideoEngine engine, CameraDirectoryService directory, ILogger logger) - : this(engine, directory, logger) + public CameraEditorViewModel(Camera existing, CameraCredentials? credentials, IVideoEngine engine, CameraDirectoryService directory, UserSettingsService userSettings, ILogger logger) + : this(engine, directory, userSettings, logger) { EditingId = existing.Id; Name = existing.Name; @@ -112,7 +114,10 @@ private async Task TestConnectionAsync() var creds = string.IsNullOrEmpty(Username) && string.IsNullOrEmpty(Password) ? null : new CameraCredentials(Username, Password); - var options = VideoSessionOptions.Default(rtspMain, creds); + // Same transport the live view will use — a UDP-only setup used to pass + // playback but fail the test (which was hardwired to the default TCP). + var options = VideoSessionOptions.Default(rtspMain, creds) + with { Transport = ParseTransport(_userSettings?.Current.RtspTransport) }; var session = _engine.CreateSession(options); try @@ -236,6 +241,12 @@ private bool TryValidate(out bool ok, out Uri rtspMain, out Uri? rtspSub, out in ok = true; return true; } + + private static RtspTransport ParseTransport(string? s) => s?.ToLowerInvariant() switch + { + "udp" => RtspTransport.Udp, + _ => RtspTransport.Tcp, + }; } public sealed record CameraEditorResult(NewCameraRequest? NewRequest, UpdateCameraRequest? UpdateRequest); diff --git a/src/OpenIPC.Viewer.App/Views/Dialogs/CameraEditorContent.axaml b/src/OpenIPC.Viewer.App/Views/Dialogs/CameraEditorContent.axaml index 490f597..d43051f 100644 --- a/src/OpenIPC.Viewer.App/Views/Dialogs/CameraEditorContent.axaml +++ b/src/OpenIPC.Viewer.App/Views/Dialogs/CameraEditorContent.axaml @@ -99,45 +99,47 @@ - -