Share podcast devices with calls and persist network selection
Reuse camera and microphone preferences across podcast and chat-room pages. Restore available LAN/VPN addresses without persisting disclosure consent, and cover shared preferences and save failures with regression tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
ff1a016c91
commit
377afc711a
8 changed files with 236 additions and 13 deletions
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
* [PostIt] Share podcast camera/microphone preferences with chat-room calls and persist the selected LAN/VPN address. Restore only currently available network addresses without remembering address-disclosure consent; require an explicit microphone choice for calls when podcasts use no microphone.
|
||||||
* [PostIt] Remember detected podcast cameras/microphones, selected devices (including no microphone) and the default title in user settings; restore them when reopening the page and report persistence failures.
|
* [PostIt] Remember detected podcast cameras/microphones, selected devices (including no microphone) and the default title in user settings; restore them when reopening the page and report persistence failures.
|
||||||
* [Podcasts] Reuse persisted LiveFlow entities to prepare, edit and delete emissions from PostIt with a title, scheduled start and estimated duration. Keep start/stop manual and plans private; optionally publish presentation-only announcements on Yavsc.Org without opening anonymous playback. Preserve broadcast identifiers, legacy recordings and authenticated HLS/MP4 access; add a scheduling migration and lifecycle/privacy regressions.
|
* [Podcasts] Reuse persisted LiveFlow entities to prepare, edit and delete emissions from PostIt with a title, scheduled start and estimated duration. Keep start/stop manual and plans private; optionally publish presentation-only announcements on Yavsc.Org without opening anonymous playback. Preserve broadcast identifiers, legacy recordings and authenticated HLS/MP4 access; add a scheduling migration and lifecycle/privacy regressions.
|
||||||
* [PostIt/API/Yavsc.Org] Expose the private PayPal recipient setting in PostIt's performer editor and authenticated profile API; validate addresses, allow disabling with an empty value and preserve the setting for older clients. Make Web donation settings discoverable from the performer profile and podcast catalogue.
|
* [PostIt/API/Yavsc.Org] Expose the private PayPal recipient setting in PostIt's performer editor and authenticated profile API; validate addresses, allow disabling with an empty value and preserve the setting for older clients. Make Web donation settings discoverable from the performer profile and podcast catalogue.
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,14 @@ le materiel disponible en conservant les choix encore presents. Le choix
|
||||||
« sans microphone » est memorise lui aussi. Charger le titre d'une emission
|
« sans microphone » est memorise lui aussi. Charger le titre d'une emission
|
||||||
preparee ne remplace pas le titre par defaut ; une saisie explicite le modifie.
|
preparee ne remplace pas le titre par defaut ; une saisie explicite le modifie.
|
||||||
Les erreurs de sauvegarde sont affichees dans la page.
|
Les erreurs de sauvegarde sont affichees dans la page.
|
||||||
|
Les appels des salons de discussion reutilisent ces memes choix camera/micro
|
||||||
|
et enregistrent leurs changements dans les memes parametres. La page Podcasts
|
||||||
|
les recharge a son activation. Les appels exigent toujours un microphone :
|
||||||
|
le choix podcast « sans microphone » laisse donc le micro d'appel non selectionne.
|
||||||
|
L'adresse LAN/VPN choisie pour les appels est aussi memorisee, mais restauree
|
||||||
|
uniquement si elle est encore presente sur une interface locale active.
|
||||||
|
Sinon, une nouvelle selection est requise. Le consentement au partage
|
||||||
|
d'adresse avec le pair n'est jamais memorise.
|
||||||
|
|
||||||
PostIt permet de preparer, modifier et supprimer une emission avant son
|
PostIt permet de preparer, modifier et supprimer une emission avant son
|
||||||
demarrage : titre, date/heure locale, duree entiere positive en minutes et
|
demarrage : titre, date/heure locale, duree entiere positive en minutes et
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,103 @@ using Avalonia.Controls;
|
||||||
using Avalonia.Headless.XUnit;
|
using Avalonia.Headless.XUnit;
|
||||||
using PostIt.Services;
|
using PostIt.Services;
|
||||||
using PostIt.ViewModels.Chat;
|
using PostIt.ViewModels.Chat;
|
||||||
|
using PostIt.ViewModels;
|
||||||
using Yavsc.Abstract.Chat.Calls;
|
using Yavsc.Abstract.Chat.Calls;
|
||||||
|
|
||||||
namespace PostIt.Tests;
|
namespace PostIt.Tests;
|
||||||
|
|
||||||
public sealed class LanCallViewModelTests
|
public sealed class LanCallViewModelTests
|
||||||
{
|
{
|
||||||
|
[AvaloniaFact]
|
||||||
|
public async Task Calls_restore_shared_devices_and_persist_choices_and_network_address()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(Path.GetTempPath(), $"postit-call-preferences-{Guid.NewGuid():N}.json");
|
||||||
|
var previousDevices = Platform.GetLanCallDevices;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var settings = new Settings(path)
|
||||||
|
{
|
||||||
|
PodcastCameras = [new("Front", "front"), new("Rear", "rear")],
|
||||||
|
PodcastMicrophones = [new("None", ""), new("USB", "usb"), new("Built-in", "built-in")],
|
||||||
|
PodcastCameraId = "rear", PodcastMicrophoneId = "usb"
|
||||||
|
};
|
||||||
|
await using (var vm = new LanCallViewModel(new Signaling(), settings: settings))
|
||||||
|
{
|
||||||
|
Assert.Equal("rear", vm.Camera?.Value);
|
||||||
|
Assert.Equal("usb", vm.Microphone?.Value);
|
||||||
|
Assert.False(vm.AllowAddressDisclosure);
|
||||||
|
vm.Camera = vm.Cameras[0];
|
||||||
|
vm.Microphone = vm.Microphones[1];
|
||||||
|
Assert.Equal("front", settings.PodcastCameraId);
|
||||||
|
Assert.Equal("built-in", settings.PodcastMicrophoneId);
|
||||||
|
Assert.NotEmpty(vm.Addresses);
|
||||||
|
vm.Address = vm.Addresses.Last();
|
||||||
|
settings.Save();
|
||||||
|
var preferred = vm.Address;
|
||||||
|
Platform.GetLanCallDevices = () => new(
|
||||||
|
[new("Rear", "rear"), new("Front", "front")],
|
||||||
|
[new("Built-in", "built-in"), new("USB", "usb")]);
|
||||||
|
vm.RefreshDevicesCommand.Execute(null);
|
||||||
|
Assert.Equal("front", vm.Camera?.Value);
|
||||||
|
Assert.Equal("built-in", vm.Microphone?.Value);
|
||||||
|
Assert.Equal(preferred, vm.Address);
|
||||||
|
Assert.Contains(settings.PodcastMicrophones, d => d.Value == "");
|
||||||
|
|
||||||
|
var reloaded = new Settings(path);
|
||||||
|
reloaded.Authentication.Authority = "";
|
||||||
|
reloaded.Load();
|
||||||
|
await using var restored = new LanCallViewModel(new Signaling(), settings: reloaded);
|
||||||
|
Assert.Equal(preferred, restored.Address);
|
||||||
|
Assert.Equal("front", restored.Camera?.Value);
|
||||||
|
Assert.Equal("built-in", restored.Microphone?.Value);
|
||||||
|
Assert.False(restored.AllowAddressDisclosure);
|
||||||
|
settings.PodcastCameraId = "rear";
|
||||||
|
Assert.Equal("rear", vm.Camera?.Value);
|
||||||
|
settings.PodcastMicrophoneId = "";
|
||||||
|
Assert.Null(vm.Microphone);
|
||||||
|
vm.AllowAddressDisclosure = true;
|
||||||
|
vm.Room = "General";
|
||||||
|
vm.Target = "bob";
|
||||||
|
await vm.InviteCommand.ExecuteAsync(null);
|
||||||
|
Assert.Contains("microphone", vm.Status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally { Platform.GetLanCallDevices = previousDevices; File.Delete(path); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[AvaloniaFact]
|
||||||
|
public async Task Call_preference_save_failures_are_reported_without_losing_the_choice()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(Path.GetTempPath(), $"postit-call-preferences-{Guid.NewGuid():N}");
|
||||||
|
Directory.CreateDirectory(path);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var settings = new Settings(path);
|
||||||
|
await using var vm = new LanCallViewModel(new Signaling(), settings: settings);
|
||||||
|
vm.Address = null;
|
||||||
|
vm.Address = "127.0.0.1";
|
||||||
|
Assert.Equal("127.0.0.1", settings.LanCallAddress);
|
||||||
|
Assert.Contains("sauvegarde impossible", vm.Status);
|
||||||
|
Assert.True(settings.IsDirty);
|
||||||
|
}
|
||||||
|
finally { Directory.Delete(path); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[AvaloniaFact]
|
||||||
|
public async Task Missing_saved_network_address_requires_a_new_selection_and_does_not_restore_consent()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(Path.GetTempPath(), $"postit-call-preferences-{Guid.NewGuid():N}.json");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var settings = new Settings(path) { LanCallAddress = "not-a-local-address" };
|
||||||
|
await using var vm = new LanCallViewModel(new Signaling(), settings: settings);
|
||||||
|
Assert.Null(vm.Address);
|
||||||
|
Assert.False(vm.AllowAddressDisclosure);
|
||||||
|
Assert.Equal("not-a-local-address", settings.LanCallAddress);
|
||||||
|
}
|
||||||
|
finally { File.Delete(path); }
|
||||||
|
}
|
||||||
|
|
||||||
private static CallSession Session() => new(Guid.NewGuid(), "General", "alice", "bob", false,
|
private static CallSession Session() => new(Guid.NewGuid(), "General", "alice", "bob", false,
|
||||||
DateTimeOffset.UtcNow.AddMinutes(1), CallTransports.LanSrtp);
|
DateTimeOffset.UtcNow.AddMinutes(1), CallTransports.LanSrtp);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ using Avalonia.Headless.XUnit;
|
||||||
using PostIt.Services;
|
using PostIt.Services;
|
||||||
using PostIt.ViewModels;
|
using PostIt.ViewModels;
|
||||||
using Yavsc.Api.Client;
|
using Yavsc.Api.Client;
|
||||||
|
using Yavsc.Abstract.Identity;
|
||||||
|
using Yavsc.Abstract.Streaming;
|
||||||
|
|
||||||
namespace PostIt.Tests;
|
namespace PostIt.Tests;
|
||||||
|
|
||||||
|
|
@ -37,13 +39,18 @@ public sealed class PodcastPreferencesTests
|
||||||
Assert.Equal("rear", restored.PodcastCameraId);
|
Assert.Equal("rear", restored.PodcastCameraId);
|
||||||
Assert.Equal("", restored.PodcastMicrophoneId);
|
Assert.Equal("", restored.PodcastMicrophoneId);
|
||||||
Platform.GetPodcastCaptureDevices = () => throw new InvalidOperationException("Must use saved devices.");
|
Platform.GetPodcastCaptureDevices = () => throw new InvalidOperationException("Must use saved devices.");
|
||||||
await using var otherApi = new YavscApiClient(restored, new TokenStore(path + ".tokens"));
|
await using var otherApi = new PreferencesApi(restored, new TokenStore(path + ".tokens"));
|
||||||
var other = new PodcastsPageViewModel(new PodcastApiClient(otherApi, () => restored.ApiUrl), otherApi, session);
|
var other = new PodcastsPageViewModel(new PodcastApiClient(otherApi, () => restored.ApiUrl), otherApi, session);
|
||||||
Assert.Equal(2, other.Cameras.Count);
|
Assert.Equal(2, other.Cameras.Count);
|
||||||
Assert.Equal(2, other.Microphones.Count);
|
Assert.Equal(2, other.Microphones.Count);
|
||||||
Assert.Equal("rear", other.SelectedCamera?.Value);
|
Assert.Equal("rear", other.SelectedCamera?.Value);
|
||||||
Assert.Equal("", other.SelectedMicrophone?.Value);
|
Assert.Equal("", other.SelectedMicrophone?.Value);
|
||||||
Assert.Equal(vm.Title, other.Title);
|
Assert.Equal(vm.Title, other.Title);
|
||||||
|
restored.PodcastCameraId = "front";
|
||||||
|
restored.PodcastMicrophoneId = "usb";
|
||||||
|
await other.ActivateAsync();
|
||||||
|
Assert.Equal("front", other.SelectedCamera?.Value);
|
||||||
|
Assert.Equal("usb", other.SelectedMicrophone?.Value);
|
||||||
|
|
||||||
Platform.GetPodcastCaptureDevices = () => new([], []);
|
Platform.GetPodcastCaptureDevices = () => new([], []);
|
||||||
other.DetectDevicesCommand.Execute(null);
|
other.DetectDevicesCommand.Execute(null);
|
||||||
|
|
@ -57,6 +64,17 @@ public sealed class PodcastPreferencesTests
|
||||||
File.Delete(path);
|
File.Delete(path);
|
||||||
File.Delete(path + ".tokens");
|
File.Delete(path + ".tokens");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PreferencesApi(Settings settings, TokenStore store) : YavscApiClient(settings, store)
|
||||||
|
{
|
||||||
|
public override Task<T> CallAsync<T>(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
object response = path.EndsWith("/account/me", StringComparison.Ordinal)
|
||||||
|
? new Me { UserId = "owner" } : new List<PodcastResponse>();
|
||||||
|
return Task.FromResult((T)response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[AvaloniaFact]
|
[AvaloniaFact]
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,7 @@ namespace PostIt.ViewModels.Chat
|
||||||
|
|
||||||
Calls = new CallSignalingClient(_connection);
|
Calls = new CallSignalingClient(_connection);
|
||||||
if (SupportsLanCalls)
|
if (SupportsLanCalls)
|
||||||
LanCall = new LanCallViewModel(Calls) { Room = SelectedRoom ?? "" };
|
LanCall = new LanCallViewModel(Calls, settings: _settings) { Room = SelectedRoom ?? "" };
|
||||||
else
|
else
|
||||||
Calls.Invitation += call => Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
Calls.Invitation += call => Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||||
Log($"Invitation audio-vidéo de {call.Caller} dans {call.RoomName} ; transport non disponible sur cette plateforme."));
|
Log($"Invitation audio-vidéo de {call.Caller} dans {call.RoomName} ; transport non disponible sur cette plateforme."));
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ public partial class LanCallViewModel : ObservableObject, IAsyncDisposable
|
||||||
private string? _activeMicrophone;
|
private string? _activeMicrophone;
|
||||||
private string? _activeAddress;
|
private string? _activeAddress;
|
||||||
private int _activePort;
|
private int _activePort;
|
||||||
|
private readonly Settings? _settings;
|
||||||
|
private bool _restoringPreferences;
|
||||||
|
|
||||||
public ObservableCollection<string> Addresses { get; } = [];
|
public ObservableCollection<string> Addresses { get; } = [];
|
||||||
public ObservableCollection<CaptureDevice> Cameras { get; } = [];
|
public ObservableCollection<CaptureDevice> Cameras { get; } = [];
|
||||||
|
|
@ -53,9 +55,11 @@ public partial class LanCallViewModel : ObservableObject, IAsyncDisposable
|
||||||
[ObservableProperty] public partial Control? RemoteVideo { get; set; }
|
[ObservableProperty] public partial Control? RemoteVideo { get; set; }
|
||||||
public bool CanConfigure => !Busy || Incoming;
|
public bool CanConfigure => !Busy || Incoming;
|
||||||
|
|
||||||
public LanCallViewModel(ILanCallSignaling signaling, Func<string, int, ILanCallMedia>? createMedia = null)
|
public LanCallViewModel(ILanCallSignaling signaling, Func<string, int, ILanCallMedia>? createMedia = null,
|
||||||
|
Settings? settings = null)
|
||||||
{
|
{
|
||||||
_signaling = signaling;
|
_signaling = signaling;
|
||||||
|
_settings = settings;
|
||||||
_createMedia = createMedia ?? Platform.CreateLanCallMedia ??
|
_createMedia = createMedia ?? Platform.CreateLanCallMedia ??
|
||||||
((address, port) => new LinuxLanCallVideo(address, port));
|
((address, port) => new LinuxLanCallVideo(address, port));
|
||||||
signaling.Invitation += OnInvitation;
|
signaling.Invitation += OnInvitation;
|
||||||
|
|
@ -64,22 +68,98 @@ public partial class LanCallViewModel : ObservableObject, IAsyncDisposable
|
||||||
signaling.Ended += OnEnded;
|
signaling.Ended += OnEnded;
|
||||||
signaling.Interrupted += OnInterrupted;
|
signaling.Interrupted += OnInterrupted;
|
||||||
Platform.PlaybackSuspending += OnSuspending;
|
Platform.PlaybackSuspending += OnSuspending;
|
||||||
|
if (settings != null)
|
||||||
|
{
|
||||||
|
_restoringPreferences = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var device in settings.PodcastCameras) Cameras.Add(device);
|
||||||
|
foreach (var device in settings.PodcastMicrophones.Where(d => !string.IsNullOrEmpty(d.Value)))
|
||||||
|
Microphones.Add(device);
|
||||||
|
RestoreDevices();
|
||||||
|
RefreshAddresses();
|
||||||
|
}
|
||||||
|
catch (NetworkInformationException ex) { Status = $"Adresses reseau indisponibles : {ex.Message}"; }
|
||||||
|
finally { _restoringPreferences = false; }
|
||||||
|
settings.PropertyChanged += OnSettingsChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSettingsChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs args)
|
||||||
|
{
|
||||||
|
if (_restoringPreferences || !CanConfigure) return;
|
||||||
|
if (args.PropertyName is not (nameof(Settings.PodcastCameraId) or nameof(Settings.PodcastMicrophoneId)
|
||||||
|
or nameof(Settings.PodcastCameras) or nameof(Settings.PodcastMicrophones))) return;
|
||||||
|
_restoringPreferences = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Cameras.Clear();
|
||||||
|
Microphones.Clear();
|
||||||
|
foreach (var device in _settings!.PodcastCameras) Cameras.Add(device);
|
||||||
|
foreach (var device in _settings.PodcastMicrophones.Where(d => !string.IsNullOrEmpty(d.Value))) Microphones.Add(device);
|
||||||
|
RestoreDevices();
|
||||||
|
}
|
||||||
|
finally { _restoringPreferences = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RestoreDevices()
|
||||||
|
{
|
||||||
|
Camera = Cameras.FirstOrDefault(d => d.Value == _settings?.PodcastCameraId) ?? Cameras.FirstOrDefault();
|
||||||
|
Microphone = _settings?.PodcastMicrophoneId == "" ? null
|
||||||
|
: Microphones.FirstOrDefault(d => d.Value == _settings?.PodcastMicrophoneId) ?? Microphones.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshAddresses()
|
||||||
|
{
|
||||||
|
var preferred = _settings?.LanCallAddress ?? Address;
|
||||||
|
Addresses.Clear();
|
||||||
|
foreach (var address in NetworkInterface.GetAllNetworkInterfaces()
|
||||||
|
.Where(n => n.OperationalStatus == OperationalStatus.Up)
|
||||||
|
.SelectMany(n => n.GetIPProperties().UnicastAddresses)
|
||||||
|
.Select(a => a.Address.ToString()).Where(LanCallNetwork.IsPrivateIPv4)
|
||||||
|
.Distinct().OrderBy(a => IPAddress.IsLoopback(IPAddress.Parse(a))))
|
||||||
|
Addresses.Add(address);
|
||||||
|
Address = preferred == null ? Addresses.FirstOrDefault() : Addresses.FirstOrDefault(a => a == preferred);
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnCameraChanged(CaptureDevice? value)
|
||||||
|
{
|
||||||
|
if (_restoringPreferences || _settings == null || value == null) return;
|
||||||
|
_settings.PodcastCameraId = value.Value;
|
||||||
|
SavePreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnMicrophoneChanged(CaptureDevice? value)
|
||||||
|
{
|
||||||
|
if (_restoringPreferences || _settings == null || value == null) return;
|
||||||
|
_settings.PodcastMicrophoneId = value.Value;
|
||||||
|
SavePreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnAddressChanged(string? value)
|
||||||
|
{
|
||||||
|
if (_restoringPreferences || _settings == null || value == null) return;
|
||||||
|
_settings.LanCallAddress = value;
|
||||||
|
SavePreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SavePreferences()
|
||||||
|
{
|
||||||
|
try { _settings?.Save(); }
|
||||||
|
catch (Exception ex) when (ex is System.IO.IOException or UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
Status = $"Preferences d'appel appliquees, mais sauvegarde impossible : {ex.Message}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void RefreshDevices()
|
private void RefreshDevices()
|
||||||
{
|
{
|
||||||
|
_restoringPreferences = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!CanConfigure) throw new InvalidOperationException("Terminez l'appel avant de changer les peripheriques.");
|
if (!CanConfigure) throw new InvalidOperationException("Terminez l'appel avant de changer les peripheriques.");
|
||||||
Addresses.Clear();
|
RefreshAddresses();
|
||||||
foreach (var address in NetworkInterface.GetAllNetworkInterfaces()
|
|
||||||
.Where(n => n.OperationalStatus == OperationalStatus.Up)
|
|
||||||
.SelectMany(n => n.GetIPProperties().UnicastAddresses)
|
|
||||||
.Select(a => a.Address.ToString()).Where(LanCallNetwork.IsPrivateIPv4)
|
|
||||||
.Distinct().OrderBy(a => IPAddress.IsLoopback(IPAddress.Parse(a))))
|
|
||||||
Addresses.Add(address);
|
|
||||||
Address = Addresses.FirstOrDefault();
|
|
||||||
Cameras.Clear();
|
Cameras.Clear();
|
||||||
Microphones.Clear();
|
Microphones.Clear();
|
||||||
if (Platform.GetLanCallDevices is { } getDevices)
|
if (Platform.GetLanCallDevices is { } getDevices)
|
||||||
|
|
@ -97,14 +177,24 @@ public partial class LanCallViewModel : ObservableObject, IAsyncDisposable
|
||||||
foreach (var device in streamer.GetCaptureDevices()) Cameras.Add(new(device.Key, device.Value));
|
foreach (var device in streamer.GetCaptureDevices()) Cameras.Add(new(device.Key, device.Value));
|
||||||
foreach (var device in streamer.GetAudioCaptureDevices()) Microphones.Add(new(device.Key, device.Value));
|
foreach (var device in streamer.GetAudioCaptureDevices()) Microphones.Add(new(device.Key, device.Value));
|
||||||
}
|
}
|
||||||
Camera = Cameras.FirstOrDefault();
|
RestoreDevices();
|
||||||
Microphone = Microphones.FirstOrDefault();
|
|
||||||
Status = Cameras.Count == 0 ? "Aucune camera detectee. Verifiez les peripheriques puis actualisez."
|
Status = Cameras.Count == 0 ? "Aucune camera detectee. Verifiez les peripheriques puis actualisez."
|
||||||
: Incoming && _call != null
|
: Incoming && _call != null
|
||||||
? $"{_call.Caller} propose un appel dans {_call.RoomName}. Selectionnez vos peripheriques puis acceptez ou refusez."
|
? $"{_call.Caller} propose un appel dans {_call.RoomName}. Selectionnez vos peripheriques puis acceptez ou refusez."
|
||||||
: "Selectionnez le pair, l'adresse LAN/VPN et les peripheriques. Utilisez un casque.";
|
: "Selectionnez le pair, l'adresse LAN/VPN et les peripheriques. Utilisez un casque.";
|
||||||
|
if (_settings != null)
|
||||||
|
{
|
||||||
|
_settings.PodcastCameras = Cameras.ToArray();
|
||||||
|
var noMicrophone = _settings.PodcastMicrophones.Where(d => string.IsNullOrEmpty(d.Value));
|
||||||
|
_settings.PodcastMicrophones = noMicrophone.Concat(Microphones).ToArray();
|
||||||
|
if (Camera != null) _settings.PodcastCameraId = Camera.Value;
|
||||||
|
if (Microphone != null) _settings.PodcastMicrophoneId = Microphone.Value;
|
||||||
|
if (Address != null) _settings.LanCallAddress = Address;
|
||||||
|
SavePreferences();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception error) { Status = "Peripheriques indisponibles : " + error.Message; }
|
catch (Exception error) { Status = "Peripheriques indisponibles : " + error.Message; }
|
||||||
|
finally { _restoringPreferences = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ValidateSelection()
|
private void ValidateSelection()
|
||||||
|
|
@ -324,6 +414,7 @@ public partial class LanCallViewModel : ObservableObject, IAsyncDisposable
|
||||||
_signaling.Ended -= OnEnded;
|
_signaling.Ended -= OnEnded;
|
||||||
_signaling.Interrupted -= OnInterrupted;
|
_signaling.Interrupted -= OnInterrupted;
|
||||||
Platform.PlaybackSuspending -= OnSuspending;
|
Platform.PlaybackSuspending -= OnSuspending;
|
||||||
|
if (_settings != null) _settings.PropertyChanged -= OnSettingsChanged;
|
||||||
try { await StopAsync("Appel arrete.", true); }
|
try { await StopAsync("Appel arrete.", true); }
|
||||||
finally { _lifetime.Dispose(); }
|
finally { _lifetime.Dispose(); }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -267,6 +267,16 @@ public partial class PodcastsPageViewModel : ViewModelBase
|
||||||
|
|
||||||
public async Task ActivateAsync()
|
public async Task ActivateAsync()
|
||||||
{
|
{
|
||||||
|
_restoringPreferences = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Cameras.Clear();
|
||||||
|
Microphones.Clear();
|
||||||
|
foreach (var device in _api.Settings.PodcastCameras) Cameras.Add(device);
|
||||||
|
foreach (var device in _api.Settings.PodcastMicrophones) Microphones.Add(device);
|
||||||
|
RestoreDeviceSelection();
|
||||||
|
}
|
||||||
|
finally { _restoringPreferences = false; }
|
||||||
if (_pageCancellation.IsCancellationRequested)
|
if (_pageCancellation.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
_pageCancellation.Dispose();
|
_pageCancellation.Dispose();
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ public partial class Settings : ViewModelBase
|
||||||
public partial string? PodcastCameraId { get; set; }
|
public partial string? PodcastCameraId { get; set; }
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial string? PodcastMicrophoneId { get; set; }
|
public partial string? PodcastMicrophoneId { get; set; }
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial string? LanCallAddress { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
|
|
@ -156,6 +158,7 @@ public partial class Settings : ViewModelBase
|
||||||
partial void OnPodcastMicrophonesChanged(CaptureDevice[] value) => MarkDirty();
|
partial void OnPodcastMicrophonesChanged(CaptureDevice[] value) => MarkDirty();
|
||||||
partial void OnPodcastCameraIdChanged(string? value) => MarkDirty();
|
partial void OnPodcastCameraIdChanged(string? value) => MarkDirty();
|
||||||
partial void OnPodcastMicrophoneIdChanged(string? value) => MarkDirty();
|
partial void OnPodcastMicrophoneIdChanged(string? value) => MarkDirty();
|
||||||
|
partial void OnLanCallAddressChanged(string? value) => MarkDirty();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authentication can be reassigned wholesale by
|
/// Authentication can be reassigned wholesale by
|
||||||
|
|
@ -418,6 +421,7 @@ public partial class Settings : ViewModelBase
|
||||||
PodcastMicrophones = settings.PodcastMicrophones ?? [];
|
PodcastMicrophones = settings.PodcastMicrophones ?? [];
|
||||||
PodcastCameraId = settings.PodcastCameraId;
|
PodcastCameraId = settings.PodcastCameraId;
|
||||||
PodcastMicrophoneId = settings.PodcastMicrophoneId;
|
PodcastMicrophoneId = settings.PodcastMicrophoneId;
|
||||||
|
LanCallAddress = settings.LanCallAddress;
|
||||||
if (!(settings.Authentication is null))
|
if (!(settings.Authentication is null))
|
||||||
{
|
{
|
||||||
this.Authentication = new AuthenticationSettings();
|
this.Authentication = new AuthenticationSettings();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue