feat(postit): add live capture previews on Android and Desktop

Reuse oriented Android EGL frames and duplicate Desktop video output before encoding without microphone monitoring. Cover preview lifecycle, rendering and recording with targeted tests and document the feature.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Paul Schneider 2026-10-25 01:03:36 +00:00
commit c2068f1476
Signed by: notazof
GPG key ID: AD4B9B545FBD7D12
14 changed files with 469 additions and 27 deletions

View file

@ -0,0 +1,90 @@
using System;
using Android.Views;
using Avalonia.Android;
using Avalonia.Controls;
using Avalonia.Platform;
using Avalonia.Threading;
namespace PostIt.Android.Services;
internal sealed class AndroidCapturePreview : Grid, IDisposable
{
private readonly PreviewHost _host;
private readonly TextBlock _status = new() { TextWrapping = Avalonia.Media.TextWrapping.Wrap };
internal AndroidCapturePreview(AndroidCameraFrameRotator rotator)
{
RowDefinitions = new RowDefinitions("*,Auto");
_host = new PreviewHost(rotator, ReportFailure);
Children.Add(_host);
SetRow(_status, 1);
Children.Add(_status);
}
private void ReportFailure(Exception error)
{
global::Android.Util.Log.Error("PostIt.Podcast", $"Apercu indisponible : {error}");
Dispatcher.UIThread.Post(() => _status.Text = $"Apercu indisponible : {error.Message}. La capture continue.");
}
public void Dispose() => _host.Dispose();
private sealed class PreviewHost(AndroidCameraFrameRotator rotator, Action<Exception> failed)
: NativeControlHost, IDisposable
{
private SurfaceView? _view;
private SurfaceCallback? _callback;
private bool _disposed;
protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
{
var activity = MainActivity.Current
?? throw new InvalidOperationException("Activite Android indisponible pour l'apercu.");
_view = new SurfaceView(activity);
_callback = new SurfaceCallback(this);
_view.Holder!.AddCallback(_callback);
return new AndroidViewControlHandle(_view);
}
private void SetSurface(ISurfaceHolder holder, int width, int height)
{
if (_disposed) return;
try { rotator.SetPreviewAsync(holder.Surface, width, height, failed).GetAwaiter().GetResult(); }
catch (Exception ex) { failed(ex); }
}
private void Detach()
{
try { rotator.SetPreviewAsync(null).GetAwaiter().GetResult(); }
catch (Exception ex) { failed(ex); }
}
protected override void DestroyNativeControlCore(IPlatformHandle control)
{
Detach();
if (_callback is not null)
{
_view?.Holder?.RemoveCallback(_callback);
_callback.Dispose();
_callback = null;
}
_view = null;
base.DestroyNativeControlCore(control);
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
Detach();
}
private sealed class SurfaceCallback(PreviewHost owner) : Java.Lang.Object, ISurfaceHolderCallback
{
public void SurfaceCreated(ISurfaceHolder holder) { }
public void SurfaceChanged(ISurfaceHolder holder, global::Android.Graphics.Format format, int width, int height)
=> owner.SetSurface(holder, width, height);
public void SurfaceDestroyed(ISurfaceHolder holder) => owner.Detach();
}
}
}