Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/OpenIPC.Viewer.App/Services/CameraEditorFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CameraEditorViewModel>());
new(_engine, _directory, _userSettings, _loggerFactory.CreateLogger<CameraEditorViewModel>());

public CameraEditorViewModel CreateForEdit(Camera existing, CameraCredentials? credentials) =>
new(existing, credentials, _engine, _directory, _loggerFactory.CreateLogger<CameraEditorViewModel>());
new(existing, credentials, _engine, _directory, _userSettings, _loggerFactory.CreateLogger<CameraEditorViewModel>());
}
16 changes: 15 additions & 1 deletion src/OpenIPC.Viewer.App/ViewModels/CameraLibraryPageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,14 @@ private void RefilterCameras()
_ = ProbeReachabilityAsync();
}

/// <summary>
/// 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.
/// </summary>
public Task ReprobeReachabilityAsync() => ProbeReachabilityAsync();

private async Task ProbeReachabilityAsync()
{
var rows = new System.Collections.Generic.List<CameraRowViewModel>(Cameras);
Expand Down Expand Up @@ -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;
}
Expand Down
19 changes: 15 additions & 4 deletions src/OpenIPC.Viewer.App/ViewModels/Dialogs/CameraEditorViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CameraEditorViewModel>? _logger;
private GroupId? _pendingGroupId;

Expand Down Expand Up @@ -48,15 +49,16 @@ public sealed partial class CameraEditorViewModel : ViewModelBase

public CameraEditorViewModel() { }

public CameraEditorViewModel(IVideoEngine engine, CameraDirectoryService directory, ILogger<CameraEditorViewModel> logger)
public CameraEditorViewModel(IVideoEngine engine, CameraDirectoryService directory, UserSettingsService userSettings, ILogger<CameraEditorViewModel> logger)
{
_engine = engine;
_directory = directory;
_userSettings = userSettings;
_logger = logger;
}

public CameraEditorViewModel(Camera existing, CameraCredentials? credentials, IVideoEngine engine, CameraDirectoryService directory, ILogger<CameraEditorViewModel> logger)
: this(engine, directory, logger)
public CameraEditorViewModel(Camera existing, CameraCredentials? credentials, IVideoEngine engine, CameraDirectoryService directory, UserSettingsService userSettings, ILogger<CameraEditorViewModel> logger)
: this(engine, directory, userSettings, logger)
{
EditingId = existing.Id;
Name = existing.Name;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
74 changes: 38 additions & 36 deletions src/OpenIPC.Viewer.App/Views/Dialogs/CameraEditorContent.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -99,45 +99,47 @@
</StackPanel>
</ScrollViewer>

<Grid Grid.Row="2" ColumnDefinitions="Auto,*,Auto,Auto" Margin="0,24,0,0">
<Button Grid.Column="0"
Content="{Binding [CameraEditor.Button.TestConnection], Source={x:Static svc:Localizer.Instance}}"
Command="{Binding TestConnectionCommand}"
Padding="12,8"
VerticalAlignment="Center"
Background="Transparent"
BorderBrush="{StaticResource BorderMediumBrush}"
BorderThickness="1"
CornerRadius="6"
Foreground="{StaticResource TextPrimaryBrush}" />
<TextBlock Grid.Column="1"
Text="{Binding TestStatus}"
<StackPanel Grid.Row="2" Margin="0,24,0,0" Spacing="10">
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
<Button Grid.Column="0"
Content="{Binding [CameraEditor.Button.TestConnection], Source={x:Static svc:Localizer.Instance}}"
Command="{Binding TestConnectionCommand}"
Padding="12,8"
VerticalAlignment="Center"
Background="Transparent"
BorderBrush="{StaticResource BorderMediumBrush}"
BorderThickness="1"
CornerRadius="6"
Foreground="{StaticResource TextPrimaryBrush}" />
<Button Grid.Column="2"
Name="CancelButton"
Content="{Binding [Common.Cancel], Source={x:Static svc:Localizer.Instance}}"
Padding="14,8"
Background="Transparent"
BorderBrush="{StaticResource BorderMediumBrush}"
BorderThickness="1"
CornerRadius="6"
Foreground="{StaticResource TextPrimaryBrush}"
Margin="0,0,8,0" />
<Button Grid.Column="3"
Name="SaveButton"
Content="{Binding [Common.Save], Source={x:Static svc:Localizer.Instance}}"
Padding="14,8"
Background="{StaticResource AccentBrush}"
Foreground="White"
CornerRadius="6"
IsDefault="True" />
</Grid>
<!-- Test result gets its own full-width row. It used to live in the
star column between the buttons, which collapses to ~0px on phone
widths and ellipsizes the whole message away — making "Test" look
like a silent no-op on mobile. -->
<TextBlock Text="{Binding TestStatus}"
FontSize="{StaticResource FontSizeSm}"
Foreground="{StaticResource TextSecondaryBrush}"
VerticalAlignment="Center"
Margin="10,0"
TextTrimming="CharacterEllipsis"
ToolTip.Tip="{Binding TestStatus}"
TextWrapping="Wrap"
IsVisible="{Binding TestStatus, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
<Button Grid.Column="2"
Name="CancelButton"
Content="{Binding [Common.Cancel], Source={x:Static svc:Localizer.Instance}}"
Padding="14,8"
Background="Transparent"
BorderBrush="{StaticResource BorderMediumBrush}"
BorderThickness="1"
CornerRadius="6"
Foreground="{StaticResource TextPrimaryBrush}"
Margin="0,0,8,0" />
<Button Grid.Column="3"
Name="SaveButton"
Content="{Binding [Common.Save], Source={x:Static svc:Localizer.Instance}}"
Padding="14,8"
Background="{StaticResource AccentBrush}"
Foreground="White"
CornerRadius="6"
IsDefault="True" />
</Grid>
</StackPanel>

</Grid>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,15 @@ public CameraLibraryPage()

private async void OnLoaded(object? sender, RoutedEventArgs e)
{
if (DataContext is CameraLibraryPageViewModel vm && !vm.IsLoaded)
if (DataContext is not CameraLibraryPageViewModel vm)
return;

if (!vm.IsLoaded)
await vm.LoadAsync(CancellationToken.None);
else
// Coming back to the page (e.g. from a camera view) — the list is
// already loaded, but the online/offline badges may be stale.
await vm.ReprobeReachabilityAsync();
}

private void OnCameraCardTapped(object? sender, TappedEventArgs e)
Expand Down
Loading