yavsc/src/PostIt/PostIt.Tests/AndroidLanCallTestActivity.cs

158 lines
7.7 KiB
C#
Raw Normal View History

#if ANDROID
using System;
using Stopwatch = System.Diagnostics.Stopwatch;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.OS;
using Avalonia.Controls;
using Avalonia.Threading;
using LibVLCSharp.Shared;
using PostIt.Android;
using PostIt.Android.Services;
using PostIt.Services;
using Path = System.IO.Path;
namespace PostIt.Tests;
/// <summary>Debug-only physical camera/SRTP test; no account, API or media recording.</summary>
[Activity(Name = "fr.pschneider.postit.LanCallTestActivity", Exported = true)]
[SupportedOSPlatform("android26.0")]
public sealed class AndroidLanCallTestActivity : Activity
{
protected override async void OnCreate(Bundle? state)
{
base.OnCreate(state);
var result = Path.Combine(FilesDir!.AbsolutePath, "lan-call-test-result.txt");
File.WriteAllText(result, "RUNNING");
try
{
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(55));
var outcome = await RunAsync(result, Intent?.GetStringExtra("wrongKey") == "true", timeout.Token);
File.WriteAllText(result, outcome);
}
catch (Exception error) { File.WriteAllText(result, "FAIL\n" + error); }
finally { Finish(); }
}
private async Task<string> RunAsync(string result, bool wrongKey, CancellationToken ct)
{
StartActivity(new Intent(this, typeof(MainActivity)).AddFlags(ActivityFlags.NewTask));
while (!MainActivity.IsForeground || (Avalonia.Application.Current as App)?.View == null)
await Task.Delay(100, ct);
await Task.Delay(1000, ct);
var fixture = Path.Combine(FilesDir!.AbsolutePath, "lan-call-fixture.ts");
if (!File.Exists(fixture)) throw new IOException("Explicit H.264/AAC MPEG-TS fixture required.");
using var pipe = new AnonymousPipeServerStream(PipeDirection.Out);
await using var peer = new LibVlcLanCallMedia("127.0.0.1", 35102, headless: true);
using var decoded = new DecodedFrames(peer.Receiver);
AndroidLanCallMedia? call = null;
Task? pump = null;
using var sending = CancellationTokenSource.CreateLinkedTokenSource(ct);
var source = File.OpenRead(fixture);
var app = (App)Avalonia.Application.Current!;
var navigation = app.View!.FindControl<NavigationPage>("NavRoot")
?? throw new InvalidOperationException("Main navigation unavailable.");
var attached = false;
try
{
await peer.StartReceivingAsync(ct);
await Dispatcher.UIThread.InvokeAsync(async () =>
{
call = new AndroidLanCallMedia("127.0.0.1", 35100);
var grid = new Grid { RowDefinitions = new RowDefinitions("*,*") };
grid.Children.Add(call.LocalVideo);
Grid.SetRow(call.RemoteVideo, 1);
grid.Children.Add(call.RemoteVideo);
await navigation.PushAsync(new ContentPage { Content = grid });
attached = true;
});
if (call == null) throw new InvalidOperationException("Call UI not created.");
File.WriteAllText(result, "RUNNING\nstage=return source pump");
pump = Task.Run(async () =>
{
var buffer = new byte[1316];
while (await source.ReadAsync(buffer, sending.Token) is var count && count > 0)
{
await pipe.WriteAsync(buffer.AsMemory(0, count), sending.Token);
await Task.Delay(4, sending.Token);
}
}, ct);
File.WriteAllText(result, "RUNNING\nstage=return SRTP sender");
await peer.StartEncodedSendingAsync(call.Endpoint,
pipe.ClientSafePipeHandle.DangerousGetHandle().ToInt32(), ct);
File.WriteAllText(result, "RUNNING\nstage=enumerating cameras");
var devices = AndroidLanCallMedia.GetDevices();
var front = devices.Cameras.FirstOrDefault(c => c.Name == "Camera avant")
?? throw new InvalidOperationException("Front camera required; no rear-camera fallback.");
var endpoint = wrongKey ? peer.Endpoint with { Key = new string('0', 32) } : peer.Endpoint;
File.WriteAllText(result, "RUNNING\nstage=front camera and SRTP");
await Dispatcher.UIThread.InvokeAsync(() => call.StartSendingAsync(endpoint, front.Value, "microphone", ct));
File.WriteAllText(result, "RUNNING\nstage=receiving camera and return media");
var elapsed = Stopwatch.StartNew();
while (elapsed.Elapsed < TimeSpan.FromSeconds(wrongKey ? 10 : 25))
{
if (call.Completion.IsCompleted) await call.Completion;
if (peer.Completion.IsCompleted) await peer.Completion;
if (!wrongKey && elapsed.Elapsed >= TimeSpan.FromSeconds(10) &&
decoded.Frames >= 100 && decoded.Samples > 144000 && call.HasRemoteMedia) break;
await Task.Delay(100, ct);
}
if (!call.HasRemoteMedia) throw new IOException("Android did not decode return audio/video.");
if (wrongKey ? decoded.Frames != 0 || decoded.Samples != 0
: decoded.Frames < 100 || decoded.Samples <= 144000)
throw new IOException($"Unexpected camera decode: frames={decoded.Frames}, samples={decoded.Samples}.");
return $"PASS\nfrontCamera=true\nwrongKey={wrongKey}\n" +
$"cameraFrames={decoded.Frames}\ncameraAudioSamples={decoded.Samples}\nelapsedMs={elapsed.ElapsedMilliseconds}\nreturnAudioVideo=true\n" +
"transport=SRTP/UDP loopback\npreview=attached\nsource=camera+microphone,return=explicit fixture\ncleanup=complete";
}
finally
{
sending.Cancel();
File.WriteAllText(result, "RUNNING\nstage=stop return source");
if (pump != null)
{
try { await pump; }
catch (System.OperationCanceledException) when (sending.IsCancellationRequested) { }
}
File.WriteAllText(result, "RUNNING\nstage=dispose call");
if (call != null) await Dispatcher.UIThread.InvokeAsync(async () => await call.DisposeAsync());
File.WriteAllText(result, "RUNNING\nstage=stop return peer");
await peer.StopAsync();
source.Dispose();
File.WriteAllText(result, "RUNNING\nstage=detach test page");
if (attached) await Dispatcher.UIThread.InvokeAsync(async () => await navigation.PopAsync());
}
}
private sealed class DecodedFrames : IDisposable
{
private readonly MediaPlayer _player;
private readonly IntPtr _buffer = Marshal.AllocHGlobal(640 * 640 * 4);
private int _frames;
private long _samples;
public int Frames => Volatile.Read(ref _frames);
public long Samples => Interlocked.Read(ref _samples);
public DecodedFrames(MediaPlayer player)
{
_player = player;
player.SetVideoFormat("RV32", 640, 640, 640 * 4);
player.SetVideoCallbacks((_, planes) =>
{
Marshal.WriteIntPtr(planes, _buffer);
return IntPtr.Zero;
}, null, (_, _) => Interlocked.Increment(ref _frames));
player.SetAudioFormat("S16N", 48000, 1);
player.SetAudioCallbacks((_, _, count, _) => Interlocked.Add(ref _samples, count),
null, null, null, null);
}
public void Dispose() { _player.Stop(); Marshal.FreeHGlobal(_buffer); }
}
}
#endif