fix(android): correct camera frame rotation and recorder timestamps
Rotate camera frames before encoding while accounting for Camera2 texture transforms. Align camera timestamps with MediaRecorder and cover capture duration, tracks and rotation with regression tests. Include LibVLC ProGuard rules and update documentation and changelog. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
dfef7652a4
commit
ed75f06fb2
14 changed files with 792 additions and 8 deletions
|
|
@ -26,6 +26,7 @@ internal sealed class AndroidPodcastCapture : IPodcastCapture
|
|||
private CameraDevice? _camera;
|
||||
private CameraCaptureSession? _session;
|
||||
private MediaRecorder? _recorder;
|
||||
private AndroidCameraFrameRotator? _rotator;
|
||||
private Surface? _surface;
|
||||
private ParcelFileDescriptor? _readPipe;
|
||||
private ParcelFileDescriptor? _writePipe;
|
||||
|
|
@ -36,6 +37,8 @@ internal sealed class AndroidPodcastCapture : IPodcastCapture
|
|||
private bool _started;
|
||||
private bool _disposed;
|
||||
public Task Completion => _completion.Task;
|
||||
internal string OrientationDiagnostics { get; private set; } = "";
|
||||
internal string? TextureTransform => _rotator?.FirstTextureTransform;
|
||||
|
||||
private AndroidPodcastCapture(PodcastCaptureService service) => _service = service;
|
||||
|
||||
|
|
@ -103,6 +106,30 @@ internal sealed class AndroidPodcastCapture : IPodcastCapture
|
|||
var size = sizes.Where(s => s.Width <= 640 && s.Height <= 480)
|
||||
.OrderByDescending(s => s.Width * s.Height).FirstOrDefault()
|
||||
?? throw new InvalidOperationException("La camera ne propose aucun format jusqu'a 480p.");
|
||||
using var sensor = characteristics.Get(CameraCharacteristics.SensorOrientation) as Java.Lang.Integer
|
||||
?? throw new InvalidOperationException("Orientation du capteur camera indisponible.");
|
||||
using var facing = characteristics.Get(CameraCharacteristics.LensFacing) as Java.Lang.Integer
|
||||
?? throw new InvalidOperationException("Position du capteur camera indisponible.");
|
||||
var activity = MainActivity.Current ?? throw new InvalidOperationException("Activite Android indisponible.");
|
||||
#pragma warning disable CS0618, CA1422
|
||||
var display = OperatingSystem.IsAndroidVersionAtLeast(30)
|
||||
? activity.Display : activity.WindowManager?.DefaultDisplay;
|
||||
#pragma warning restore CS0618, CA1422
|
||||
var displayRotation = display?.Rotation
|
||||
?? throw new InvalidOperationException("Orientation de l'ecran Android indisponible.");
|
||||
var displayDegrees = displayRotation switch
|
||||
{
|
||||
SurfaceOrientation.Rotation0 => 0,
|
||||
SurfaceOrientation.Rotation90 => 90,
|
||||
SurfaceOrientation.Rotation180 => 180,
|
||||
SurfaceOrientation.Rotation270 => 270,
|
||||
_ => throw new InvalidOperationException($"Rotation d'ecran Android inconnue : {displayRotation}.")
|
||||
};
|
||||
var rotation = CameraFrameOrientation.GetRotation(sensor.IntValue(), displayDegrees,
|
||||
facing.IntValue() == (int)LensFacing.Front);
|
||||
global::Android.Util.Log.Info("PostIt.Podcast",
|
||||
$"Camera orientation: sensor={sensor.IntValue()}, display={displayDegrees}, facing={facing.IntValue()}, rotation={rotation}");
|
||||
OrientationDiagnostics = $"sensor={sensor.IntValue()}, display={displayDegrees}, facing={facing.IntValue()}, rotation={rotation}";
|
||||
#pragma warning disable CS0618
|
||||
_recorder = OperatingSystem.IsAndroidVersionAtLeast(31)
|
||||
? new MediaRecorder(_service) : new MediaRecorder();
|
||||
|
|
@ -111,7 +138,8 @@ internal sealed class AndroidPodcastCapture : IPodcastCapture
|
|||
if (microphone) _recorder.SetAudioSource(AudioSource.Mic);
|
||||
_recorder.SetVideoSource(VideoSource.Surface);
|
||||
_recorder.SetOutputFormat(OutputFormat.Mpeg2Ts);
|
||||
_recorder.SetVideoSize(size.Width, size.Height);
|
||||
_recorder.SetVideoSize(rotation is 90 or 270 ? size.Height : size.Width,
|
||||
rotation is 90 or 270 ? size.Width : size.Height);
|
||||
_recorder.SetVideoFrameRate(30);
|
||||
_recorder.SetVideoEncodingBitRate(1200000);
|
||||
_recorder.SetVideoEncoder(VideoEncoder.H264);
|
||||
|
|
@ -131,20 +159,23 @@ internal sealed class AndroidPodcastCapture : IPodcastCapture
|
|||
ct.ThrowIfCancellationRequested();
|
||||
if (_stopRequested) throw new System.OperationCanceledException("Demarrage de la capture annule.");
|
||||
_surface = _recorder.Surface ?? throw new InvalidOperationException("Surface encodeur indisponible.");
|
||||
_rotator = await AndroidCameraFrameRotator.CreateAsync(_surface, size.Width, size.Height, rotation, Fail);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
if (_stopRequested) throw new System.OperationCanceledException("Demarrage de la capture annule.");
|
||||
var cameraSurface = _rotator.CameraSurface;
|
||||
var opened = new TaskCompletionSource<CameraDevice>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
manager.OpenCamera(cameraId, new DeviceCallback(this, opened), _handler);
|
||||
_camera = await opened.Task.WaitAsync(TimeSpan.FromSeconds(10), ct);
|
||||
var configured = new TaskCompletionSource<CameraCaptureSession>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
#pragma warning disable CS0618, CA1422
|
||||
_camera.CreateCaptureSession(new List<Surface> { _surface }, new SessionCallback(this, configured), _handler);
|
||||
_camera.CreateCaptureSession(new List<Surface> { cameraSurface }, new SessionCallback(this, configured), _handler);
|
||||
#pragma warning restore CS0618, CA1422
|
||||
_session = await configured.Task.WaitAsync(TimeSpan.FromSeconds(10), ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
if (_stopRequested) throw new System.OperationCanceledException("Demarrage de la capture annule.");
|
||||
using var request = _camera.CreateCaptureRequest(CameraTemplate.Record);
|
||||
request.AddTarget(_surface);
|
||||
request.AddTarget(cameraSurface);
|
||||
using var built = request.Build();
|
||||
_session.SetRepeatingRequest(built, null, _handler);
|
||||
var handle = new SafeFileHandle((IntPtr)_readPipe.Fd, ownsHandle: false);
|
||||
var stream = new FileStream(handle, FileAccess.Read);
|
||||
_pump = Task.Run(async () =>
|
||||
|
|
@ -169,6 +200,8 @@ internal sealed class AndroidPodcastCapture : IPodcastCapture
|
|||
});
|
||||
await Task.Run(_recorder.Start, ct);
|
||||
_started = true;
|
||||
await _rotator.StartAsync();
|
||||
_session.SetRepeatingRequest(built, null, _handler);
|
||||
_writePipe.Close();
|
||||
_writePipe.Dispose();
|
||||
_writePipe = null;
|
||||
|
|
@ -261,7 +294,11 @@ internal sealed class AndroidPodcastCapture : IPodcastCapture
|
|||
|
||||
private void StopNative()
|
||||
{
|
||||
try { if (_started) _recorder?.Stop(); }
|
||||
try
|
||||
{
|
||||
// Keep frames flowing until MediaRecorder has drained its encoder.
|
||||
if (_started) _recorder?.Stop();
|
||||
}
|
||||
catch (Java.Lang.RuntimeException ex)
|
||||
{
|
||||
_failure = ex;
|
||||
|
|
@ -276,6 +313,8 @@ internal sealed class AndroidPodcastCapture : IPodcastCapture
|
|||
_camera?.Close();
|
||||
_camera?.Dispose();
|
||||
_camera = null;
|
||||
_rotator?.Dispose();
|
||||
_rotator = null;
|
||||
if (_recorder is not null)
|
||||
{
|
||||
_recorder.Error -= OnRecorderError;
|
||||
|
|
|
|||
Loading…
Reference in a new issue