screen orientation and awake
This commit is contained in:
parent
256fd5738c
commit
fa8038c676
25 changed files with 941 additions and 110 deletions
|
|
@ -48,6 +48,9 @@ internal sealed class AndroidCameraFrameRotator : IDisposable
|
|||
private int _previewHeight;
|
||||
private int _outputWidth;
|
||||
private int _outputHeight;
|
||||
private int _inputWidth;
|
||||
private int _inputHeight;
|
||||
private bool _previewOnly;
|
||||
private SurfaceTexture? _texture;
|
||||
private FrameListener? _listener;
|
||||
private Surface? _cameraSurface;
|
||||
|
|
@ -59,7 +62,7 @@ internal sealed class AndroidCameraFrameRotator : IDisposable
|
|||
private bool _rendering;
|
||||
private bool _disposed;
|
||||
private int _rotation;
|
||||
private int? _remainingRotation;
|
||||
private int _remainingRotation = -1;
|
||||
internal string? FirstTextureTransform { get; private set; }
|
||||
|
||||
internal Surface CameraSurface => _cameraSurface
|
||||
|
|
@ -73,7 +76,7 @@ internal sealed class AndroidCameraFrameRotator : IDisposable
|
|||
}
|
||||
|
||||
internal static async Task<AndroidCameraFrameRotator> CreateAsync(
|
||||
Surface encoderSurface, int width, int height, int rotation, Action<Exception> failed)
|
||||
Surface? encoderSurface, int width, int height, int rotation, Action<Exception> failed)
|
||||
{
|
||||
var rotator = new AndroidCameraFrameRotator(failed);
|
||||
try
|
||||
|
|
@ -88,9 +91,12 @@ internal sealed class AndroidCameraFrameRotator : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private void Initialize(Surface encoderSurface, int width, int height, int rotation)
|
||||
private void Initialize(Surface? encoderSurface, int width, int height, int rotation)
|
||||
{
|
||||
_rotation = rotation;
|
||||
_inputWidth = width;
|
||||
_inputHeight = height;
|
||||
_previewOnly = encoderSurface is null;
|
||||
_display = EGL14.EglGetDisplay(EGL14.EglDefaultDisplay);
|
||||
if (_display is null || _display.Equals(EGL14.EglNoDisplay))
|
||||
throw EglError("Obtention de l'affichage EGL");
|
||||
|
|
@ -100,7 +106,7 @@ internal sealed class AndroidCameraFrameRotator : IDisposable
|
|||
[
|
||||
EGL14.EglRedSize, 8, EGL14.EglGreenSize, 8, EGL14.EglBlueSize, 8,
|
||||
EGL14.EglRenderableType, EGL14.EglOpenglEs2Bit,
|
||||
EGL14.EglSurfaceType, EGL14.EglWindowBit, RecordableAndroid, 1, EGL14.EglNone
|
||||
EGL14.EglSurfaceType, EGL14.EglWindowBit | EGL14.EglPbufferBit, RecordableAndroid, 1, EGL14.EglNone
|
||||
];
|
||||
var configs = new EGLConfig[1];
|
||||
var count = new int[1];
|
||||
|
|
@ -111,7 +117,10 @@ internal sealed class AndroidCameraFrameRotator : IDisposable
|
|||
[EGL14.EglContextClientVersion, 2, EGL14.EglNone], 0);
|
||||
if (_context is null || _context.Equals(EGL14.EglNoContext))
|
||||
throw EglError("Creation du contexte EGL");
|
||||
_window = EGL14.EglCreateWindowSurface(_display, configs[0], encoderSurface, [EGL14.EglNone], 0);
|
||||
_window = encoderSurface is null
|
||||
? EGL14.EglCreatePbufferSurface(_display, configs[0],
|
||||
[EGL14.EglWidth, 1, EGL14.EglHeight, 1, EGL14.EglNone], 0)
|
||||
: EGL14.EglCreateWindowSurface(_display, configs[0], encoderSurface, [EGL14.EglNone], 0);
|
||||
if (_window is null || _window.Equals(EGL14.EglNoSurface))
|
||||
throw EglError("Creation de la surface EGL");
|
||||
if (!EGL14.EglMakeCurrent(_display, _window, _window, _context))
|
||||
|
|
@ -127,7 +136,7 @@ internal sealed class AndroidCameraFrameRotator : IDisposable
|
|||
GLES20.GlTexParameteri(ExternalTexture, GLES20.GlTextureWrapS, GLES20.GlClampToEdge);
|
||||
GLES20.GlTexParameteri(ExternalTexture, GLES20.GlTextureWrapT, GLES20.GlClampToEdge);
|
||||
_vertices = CreateBuffer([-1, -1, 1, -1, -1, 1, 1, 1]);
|
||||
_coordinates = CreateBuffer(CameraFrameOrientation.GetTextureCoordinates(0));
|
||||
_coordinates = CreateBuffer(CameraFrameOrientation.GetTextureCoordinates(rotation));
|
||||
GLES20.GlUseProgram(_program);
|
||||
BindAttribute("position", _vertices);
|
||||
BindAttribute("textureCoordinate", _coordinates);
|
||||
|
|
@ -147,6 +156,12 @@ internal sealed class AndroidCameraFrameRotator : IDisposable
|
|||
|
||||
internal Task StartAsync() => InvokeAsync(() => _rendering = true);
|
||||
|
||||
internal Task SetRotationAsync(int rotation) => InvokeAsync(() =>
|
||||
{
|
||||
_rotation = rotation;
|
||||
_remainingRotation = -1;
|
||||
});
|
||||
|
||||
internal Task SetPreviewAsync(Surface? surface, int width = 0, int height = 0, Action<Exception>? failed = null)
|
||||
{
|
||||
lock (_lifecycle)
|
||||
|
|
@ -173,7 +188,6 @@ internal sealed class AndroidCameraFrameRotator : IDisposable
|
|||
{
|
||||
if (!EGL14.EglMakeCurrent(_display, _window, _window, _context))
|
||||
throw EglError("Activation de l'encodeur");
|
||||
GLES20.GlViewport(0, 0, _outputWidth, _outputHeight);
|
||||
texture.UpdateTexImage();
|
||||
var presentationTime = _clock.GetPresentationTime(texture.Timestamp, Java.Lang.JavaSystem.NanoTime());
|
||||
if (presentationTime is null) return;
|
||||
|
|
@ -187,15 +201,19 @@ internal sealed class AndroidCameraFrameRotator : IDisposable
|
|||
_coordinates.Put(CameraFrameOrientation.GetTextureCoordinates(remainingRotation));
|
||||
_coordinates.Position(0);
|
||||
global::Android.Util.Log.Info("PostIt.Podcast",
|
||||
$"Camera texture transform: [{FirstTextureTransform}], remaining rotation={remainingRotation}");
|
||||
$"Camera texture transform: [{FirstTextureTransform}], sensor/display rotation={_rotation}, remaining rotation={remainingRotation}");
|
||||
}
|
||||
GLES20.GlUniformMatrix4fv(_matrixLocation, 1, false, _textureMatrix, 0);
|
||||
GLES20.GlDrawArrays(GLES20.GlTriangleStrip, 0, 4);
|
||||
CheckGl();
|
||||
if (!EGLExt.EglPresentationTimeANDROID(_display, _window, presentationTime.Value))
|
||||
throw EglError("Horodatage de l'image camera");
|
||||
if (!EGL14.EglSwapBuffers(_display, _window))
|
||||
throw EglError("Envoi de l'image a l'encodeur");
|
||||
if (!_previewOnly)
|
||||
{
|
||||
SetViewport(_outputWidth, _outputHeight);
|
||||
GLES20.GlDrawArrays(GLES20.GlTriangleStrip, 0, 4);
|
||||
CheckGl();
|
||||
if (!EGLExt.EglPresentationTimeANDROID(_display, _window, presentationTime.Value))
|
||||
throw EglError("Horodatage de l'image camera");
|
||||
if (!EGL14.EglSwapBuffers(_display, _window))
|
||||
throw EglError("Envoi de l'image a l'encodeur");
|
||||
}
|
||||
DrawPreview();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -212,12 +230,7 @@ internal sealed class AndroidCameraFrameRotator : IDisposable
|
|||
{
|
||||
if (!EGL14.EglMakeCurrent(_display, _previewWindow, _previewWindow, _context))
|
||||
throw EglError("Activation de l'apercu");
|
||||
GLES20.GlClearColor(0, 0, 0, 1);
|
||||
GLES20.GlClear(GLES20.GlColorBufferBit);
|
||||
var scale = Math.Min((double)_previewWidth / _outputWidth, (double)_previewHeight / _outputHeight);
|
||||
var width = Math.Max(1, (int)(_outputWidth * scale));
|
||||
var height = Math.Max(1, (int)(_outputHeight * scale));
|
||||
GLES20.GlViewport((_previewWidth - width) / 2, (_previewHeight - height) / 2, width, height);
|
||||
SetViewport(_previewWidth, _previewHeight);
|
||||
GLES20.GlDrawArrays(GLES20.GlTriangleStrip, 0, 4);
|
||||
CheckGl();
|
||||
if (!EGL14.EglSwapBuffers(_display, _previewWindow))
|
||||
|
|
@ -232,6 +245,18 @@ internal sealed class AndroidCameraFrameRotator : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private void SetViewport(int targetWidth, int targetHeight)
|
||||
{
|
||||
GLES20.GlClearColor(0, 0, 0, 1);
|
||||
GLES20.GlClear(GLES20.GlColorBufferBit);
|
||||
var contentWidth = _rotation is 90 or 270 ? _inputHeight : _inputWidth;
|
||||
var contentHeight = _rotation is 90 or 270 ? _inputWidth : _inputHeight;
|
||||
var scale = Math.Min((double)targetWidth / contentWidth, (double)targetHeight / contentHeight);
|
||||
var width = Math.Max(1, (int)(contentWidth * scale));
|
||||
var height = Math.Max(1, (int)(contentHeight * scale));
|
||||
GLES20.GlViewport((targetWidth - width) / 2, (targetHeight - height) / 2, width, height);
|
||||
}
|
||||
|
||||
private void ReleasePreview()
|
||||
{
|
||||
if (_previewWindow is not null)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
|||
using Android.Content;
|
||||
using Android.Hardware.Camera2;
|
||||
using Android.Hardware.Camera2.Params;
|
||||
using Android.Hardware.Display;
|
||||
using Android.Media;
|
||||
using Android.OS;
|
||||
using Android.Views;
|
||||
|
|
@ -20,7 +21,13 @@ namespace PostIt.Android.Services;
|
|||
[SupportedOSPlatform("android26.0")]
|
||||
internal sealed class AndroidPodcastCapture : IPodcastCapture
|
||||
{
|
||||
private readonly PodcastCaptureService _service;
|
||||
private static readonly SemaphoreSlim CaptureGate = new(1, 1);
|
||||
private readonly PodcastCaptureService? _service;
|
||||
private readonly Context _context;
|
||||
private DisplayManager? _displayManager;
|
||||
private RotationListener? _rotationListener;
|
||||
private int _sensorDegrees;
|
||||
private bool _frontFacing;
|
||||
private readonly SemaphoreSlim _stopGate = new(1);
|
||||
private readonly TaskCompletionSource _completion = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private readonly Handler _handler = new(Looper.MainLooper!);
|
||||
|
|
@ -44,7 +51,11 @@ internal sealed class AndroidPodcastCapture : IPodcastCapture
|
|||
internal string OrientationDiagnostics { get; private set; } = "";
|
||||
internal string? TextureTransform => _rotator?.FirstTextureTransform;
|
||||
|
||||
private AndroidPodcastCapture(PodcastCaptureService service) => _service = service;
|
||||
private AndroidPodcastCapture(Context context, PodcastCaptureService? service = null)
|
||||
{
|
||||
_context = context;
|
||||
_service = service;
|
||||
}
|
||||
|
||||
private static CameraManager GetManager(Context context)
|
||||
=> context.GetSystemService(Context.CameraService) as CameraManager
|
||||
|
|
@ -76,6 +87,28 @@ internal sealed class AndroidPodcastCapture : IPodcastCapture
|
|||
=> await StartCoreAsync(camera, microphone,
|
||||
(stream, token) => new TransportStreamCapture(directory).RunAsync(stream, token), null, null, ct);
|
||||
|
||||
public static async Task<IPodcastCapture> StartPreviewAsync(string camera, CancellationToken ct)
|
||||
{
|
||||
var activity = MainActivity.Current ?? throw new InvalidOperationException("Activite Android indisponible.");
|
||||
await PodcastPermissions.EnsureAsync(activity, false, ct, notifications: false);
|
||||
if (!MainActivity.IsForeground)
|
||||
throw new InvalidOperationException("Revenez dans PostIt pour previsualiser la camera.");
|
||||
if (!await CaptureGate.WaitAsync(0, ct))
|
||||
throw new InvalidOperationException("La camera est deja utilisee par un apercu, un podcast ou un appel.");
|
||||
var capture = new AndroidPodcastCapture(activity);
|
||||
Platform.PlaybackSuspending += capture.RequestStop;
|
||||
try
|
||||
{
|
||||
await capture.StartNativeAsync(camera, false, null, null, ct);
|
||||
return capture;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await capture.DisposeAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
internal static Task<IPodcastCapture> StartCallAsync(string camera, string microphone,
|
||||
Func<int, CancellationToken, Task> output, Func<Task> stopOutput, CancellationToken ct) =>
|
||||
StartCoreAsync(camera, microphone, null, output, stopOutput, ct);
|
||||
|
|
@ -86,8 +119,16 @@ internal sealed class AndroidPodcastCapture : IPodcastCapture
|
|||
{
|
||||
await RequestPermissionsAsync(microphone, ct);
|
||||
var activity = MainActivity.Current ?? throw new InvalidOperationException("Activite Android indisponible.");
|
||||
var service = await PodcastCaptureService.StartAsync(activity, microphone is not null, ct, pipeOutput != null);
|
||||
var capture = new AndroidPodcastCapture(service) { _stopOutput = stopOutput };
|
||||
if (!await CaptureGate.WaitAsync(0, ct))
|
||||
throw new InvalidOperationException("La camera est deja utilisee par un apercu, un podcast ou un appel.");
|
||||
PodcastCaptureService service;
|
||||
try { service = await PodcastCaptureService.StartAsync(activity, microphone is not null, ct, pipeOutput != null); }
|
||||
catch
|
||||
{
|
||||
CaptureGate.Release();
|
||||
throw;
|
||||
}
|
||||
var capture = new AndroidPodcastCapture(service, service) { _stopOutput = stopOutput };
|
||||
service.Capture = capture;
|
||||
try
|
||||
{
|
||||
|
|
@ -115,7 +156,7 @@ internal sealed class AndroidPodcastCapture : IPodcastCapture
|
|||
Func<Stream, CancellationToken, Task>? output,
|
||||
Func<int, CancellationToken, Task>? pipeOutput, CancellationToken ct)
|
||||
{
|
||||
var manager = GetManager(_service);
|
||||
var manager = GetManager(_context);
|
||||
using var characteristics = manager.GetCameraCharacteristics(cameraId);
|
||||
using var formats = characteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap) as StreamConfigurationMap
|
||||
?? throw new InvalidOperationException("Formats camera indisponibles.");
|
||||
|
|
@ -128,55 +169,42 @@ internal sealed class AndroidPodcastCapture : IPodcastCapture
|
|||
?? 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
|
||||
_sensorDegrees = sensor.IntValue();
|
||||
_frontFacing = facing.IntValue() == (int)LensFacing.Front;
|
||||
var rotation = GetRotation();
|
||||
var previewOnly = output is null && pipeOutput is null;
|
||||
if (!previewOnly)
|
||||
{
|
||||
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();
|
||||
_recorder = OperatingSystem.IsAndroidVersionAtLeast(31)
|
||||
? new MediaRecorder(_context) : new MediaRecorder();
|
||||
#pragma warning restore CS0618
|
||||
_recorder.Error += OnRecorderError;
|
||||
if (microphone) _recorder.SetAudioSource(AudioSource.Mic);
|
||||
_recorder.SetVideoSource(VideoSource.Surface);
|
||||
_recorder.SetOutputFormat(OutputFormat.Mpeg2Ts);
|
||||
_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);
|
||||
if (microphone)
|
||||
{
|
||||
_recorder.SetAudioSamplingRate(48000);
|
||||
_recorder.SetAudioChannels(1);
|
||||
_recorder.SetAudioEncodingBitRate(96000);
|
||||
_recorder.SetAudioEncoder(AudioEncoder.Aac);
|
||||
_recorder.Error += OnRecorderError;
|
||||
if (microphone) _recorder.SetAudioSource(AudioSource.Mic);
|
||||
_recorder.SetVideoSource(VideoSource.Surface);
|
||||
_recorder.SetOutputFormat(OutputFormat.Mpeg2Ts);
|
||||
_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);
|
||||
if (microphone)
|
||||
{
|
||||
_recorder.SetAudioSamplingRate(48000);
|
||||
_recorder.SetAudioChannels(1);
|
||||
_recorder.SetAudioEncodingBitRate(96000);
|
||||
_recorder.SetAudioEncoder(AudioEncoder.Aac);
|
||||
}
|
||||
var pipe = ParcelFileDescriptor.CreatePipe()
|
||||
?? throw new IOException("Creation du canal MPEG-TS impossible.");
|
||||
_readPipe = pipe[0];
|
||||
_writePipe = pipe[1];
|
||||
_recorder.SetOutputFile(_writePipe.FileDescriptor);
|
||||
await Task.Run(_recorder.Prepare, ct);
|
||||
_surface = _recorder.Surface ?? throw new InvalidOperationException("Surface encodeur indisponible.");
|
||||
}
|
||||
var pipe = ParcelFileDescriptor.CreatePipe()
|
||||
?? throw new IOException("Creation du canal MPEG-TS impossible.");
|
||||
_readPipe = pipe[0];
|
||||
_writePipe = pipe[1];
|
||||
_recorder.SetOutputFile(_writePipe.FileDescriptor);
|
||||
await Task.Run(_recorder.Prepare, ct);
|
||||
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);
|
||||
_preview = new AndroidCapturePreview(_rotator);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
|
@ -192,15 +220,14 @@ internal sealed class AndroidPodcastCapture : IPodcastCapture
|
|||
_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);
|
||||
using var request = _camera.CreateCaptureRequest(previewOnly ? CameraTemplate.Preview : CameraTemplate.Record);
|
||||
request.AddTarget(cameraSurface);
|
||||
using var built = request.Build();
|
||||
if (pipeOutput != null)
|
||||
await pipeOutput(_readPipe.Fd, ct);
|
||||
else
|
||||
await pipeOutput(_readPipe!.Fd, ct);
|
||||
else if (output is not null)
|
||||
{
|
||||
var consume = output ?? throw new InvalidOperationException("Destination de capture absente.");
|
||||
var handle = new SafeFileHandle((IntPtr)_readPipe.Fd, ownsHandle: false);
|
||||
var handle = new SafeFileHandle((IntPtr)_readPipe!.Fd, ownsHandle: false);
|
||||
var stream = new FileStream(handle, FileAccess.Read);
|
||||
_pump = Task.Run(async () =>
|
||||
{
|
||||
|
|
@ -209,7 +236,7 @@ internal sealed class AndroidPodcastCapture : IPodcastCapture
|
|||
{
|
||||
try
|
||||
{
|
||||
await consume(stream, CancellationToken.None);
|
||||
await output(stream, CancellationToken.None);
|
||||
if (_failure is not null) throw new IOException("Capture Android interrompue.", _failure);
|
||||
if (!_stopped) throw new IOException("Le flux de capture Android s'est ferme prematurement.");
|
||||
_completion.TrySetResult();
|
||||
|
|
@ -223,15 +250,63 @@ internal sealed class AndroidPodcastCapture : IPodcastCapture
|
|||
}
|
||||
});
|
||||
}
|
||||
await Task.Run(_recorder.Start, ct);
|
||||
_started = true;
|
||||
if (_recorder is not null)
|
||||
{
|
||||
await Task.Run(_recorder.Start, ct);
|
||||
_started = true;
|
||||
}
|
||||
_displayManager = _context.GetSystemService(Context.DisplayService) as DisplayManager
|
||||
?? throw new InvalidOperationException("Service d'affichage Android indisponible.");
|
||||
_rotationListener = new RotationListener(this);
|
||||
_displayManager.RegisterDisplayListener(_rotationListener, _handler);
|
||||
await _rotator.SetRotationAsync(GetRotation());
|
||||
await _rotator.StartAsync();
|
||||
_session.SetRepeatingRequest(built, null, _handler);
|
||||
_writePipe.Close();
|
||||
_writePipe.Dispose();
|
||||
_writePipe?.Close();
|
||||
_writePipe?.Dispose();
|
||||
_writePipe = null;
|
||||
}
|
||||
|
||||
private int GetRotation()
|
||||
{
|
||||
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(_sensorDegrees, displayDegrees, _frontFacing);
|
||||
global::Android.Util.Log.Info("PostIt.Podcast",
|
||||
$"Camera orientation: sensor={_sensorDegrees}, display={displayDegrees}, front={_frontFacing}, rotation={rotation}");
|
||||
OrientationDiagnostics = $"sensor={_sensorDegrees}, display={displayDegrees}, front={_frontFacing}, rotation={rotation}";
|
||||
return rotation;
|
||||
}
|
||||
|
||||
private sealed class RotationListener(AndroidPodcastCapture capture) : Java.Lang.Object, DisplayManager.IDisplayListener
|
||||
{
|
||||
public void OnDisplayAdded(int displayId) { }
|
||||
public void OnDisplayRemoved(int displayId) { }
|
||||
public async void OnDisplayChanged(int displayId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!capture._stopRequested && MainActivity.Current is { IsDestroyed: false }
|
||||
&& capture._rotator is { } rotator)
|
||||
await rotator.SetRotationAsync(capture.GetRotation());
|
||||
}
|
||||
catch (Exception ex) { capture.Fail(ex); }
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DeviceCallback(AndroidPodcastCapture capture, TaskCompletionSource<CameraDevice> opened)
|
||||
: CameraDevice.StateCallback
|
||||
{
|
||||
|
|
@ -368,6 +443,14 @@ internal sealed class AndroidPodcastCapture : IPodcastCapture
|
|||
finally
|
||||
{
|
||||
_started = false;
|
||||
if (_rotationListener is not null)
|
||||
{
|
||||
_displayManager?.UnregisterDisplayListener(_rotationListener);
|
||||
_rotationListener.Dispose();
|
||||
_rotationListener = null;
|
||||
}
|
||||
_displayManager?.Dispose();
|
||||
_displayManager = null;
|
||||
_session?.Close();
|
||||
_session?.Dispose();
|
||||
_session = null;
|
||||
|
|
@ -398,12 +481,17 @@ internal sealed class AndroidPodcastCapture : IPodcastCapture
|
|||
try { await StopAsync(); }
|
||||
finally
|
||||
{
|
||||
Platform.PlaybackSuspending -= RequestStop;
|
||||
_readPipe?.Dispose();
|
||||
_readPipe = null;
|
||||
_service.Capture = null;
|
||||
_service.StopSelf();
|
||||
if (_service is not null)
|
||||
{
|
||||
_service.Capture = null;
|
||||
_service.StopSelf();
|
||||
}
|
||||
_handler.Dispose();
|
||||
_stopGate.Dispose();
|
||||
CaptureGate.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ internal sealed class AndroidVideoSurface(MediaPlayer player, bool replay) : Nat
|
|||
private VideoView? _video;
|
||||
private ReplayLayout? _layout;
|
||||
private bool _disposed;
|
||||
private bool _playbackSubscribed;
|
||||
private IDisposable? _screenAwake;
|
||||
private readonly TaskCompletionSource _ready = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
internal Task Ready => _ready.Task;
|
||||
|
||||
|
|
@ -28,6 +30,16 @@ internal sealed class AndroidVideoSurface(MediaPlayer player, bool replay) : Nat
|
|||
?? throw new InvalidOperationException("L'activite Android est indisponible.");
|
||||
var video = new VideoView(activity) { MediaPlayer = _disposed ? null : player };
|
||||
_video = video;
|
||||
if (!_disposed)
|
||||
{
|
||||
player.Playing += OnPlaybackChanged;
|
||||
player.Paused += OnPlaybackChanged;
|
||||
player.Stopped += OnPlaybackChanged;
|
||||
player.EndReached += OnPlaybackChanged;
|
||||
player.EncounteredError += OnPlaybackChanged;
|
||||
_playbackSubscribed = true;
|
||||
}
|
||||
UpdateScreenAwake();
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (!_disposed) _ready.TrySetResult();
|
||||
|
|
@ -42,6 +54,7 @@ internal sealed class AndroidVideoSurface(MediaPlayer player, bool replay) : Nat
|
|||
|
||||
protected override void DestroyNativeControlCore(IPlatformHandle control)
|
||||
{
|
||||
ReleaseScreenAwake();
|
||||
_layout?.Stop();
|
||||
_layout = null;
|
||||
if (_video is not null) _video.MediaPlayer = null;
|
||||
|
|
@ -52,11 +65,41 @@ internal sealed class AndroidVideoSurface(MediaPlayer player, bool replay) : Nat
|
|||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
ReleaseScreenAwake();
|
||||
_ready.TrySetCanceled();
|
||||
_layout?.Stop();
|
||||
if (_video is not null) _video.MediaPlayer = null;
|
||||
}
|
||||
|
||||
private void OnPlaybackChanged(object? sender, EventArgs e)
|
||||
=> Avalonia.Threading.Dispatcher.UIThread.Post(UpdateScreenAwake);
|
||||
|
||||
private void UpdateScreenAwake()
|
||||
{
|
||||
if (!_disposed && _video is not null && player.IsPlaying)
|
||||
_screenAwake ??= Platform.ScreenAwake.Acquire();
|
||||
else
|
||||
{
|
||||
_screenAwake?.Dispose();
|
||||
_screenAwake = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleaseScreenAwake()
|
||||
{
|
||||
if (_playbackSubscribed)
|
||||
{
|
||||
player.Playing -= OnPlaybackChanged;
|
||||
player.Paused -= OnPlaybackChanged;
|
||||
player.Stopped -= OnPlaybackChanged;
|
||||
player.EndReached -= OnPlaybackChanged;
|
||||
player.EncounteredError -= OnPlaybackChanged;
|
||||
_playbackSubscribed = false;
|
||||
}
|
||||
_screenAwake?.Dispose();
|
||||
_screenAwake = null;
|
||||
}
|
||||
|
||||
private sealed class ReplayLayout : FrameLayout
|
||||
{
|
||||
private readonly MediaPlayer _player;
|
||||
|
|
|
|||
|
|
@ -37,14 +37,21 @@ internal static class CameraFrameOrientation
|
|||
throw new ArgumentException("A SurfaceTexture transform must contain 16 elements.", nameof(textureMatrix));
|
||||
// SurfaceTexture's usual Y flip changes the sign of the first column's Y.
|
||||
// Camera2 can also put the sensor rotation in this matrix; don't apply it twice.
|
||||
var angle = Math.Atan2(-textureMatrix[1], textureMatrix[0]) * 180 / Math.PI;
|
||||
// Front cameras can add a mirror: with Rot(p)·Mirror·Rot(r) = Rot(p - r)·Mirror,
|
||||
// the remaining rotation must then be applied in the opposite direction.
|
||||
var mirrored = textureMatrix[4] * textureMatrix[1] - textureMatrix[0] * textureMatrix[5] < 0;
|
||||
var angle = mirrored
|
||||
? Math.Atan2(textureMatrix[1], -textureMatrix[0]) * 180 / Math.PI
|
||||
: Math.Atan2(-textureMatrix[1], textureMatrix[0]) * 180 / Math.PI;
|
||||
if (!double.IsFinite(angle) || (textureMatrix[0] == 0 && textureMatrix[1] == 0))
|
||||
throw new ArgumentException("Invalid SurfaceTexture orientation.", nameof(textureMatrix));
|
||||
var quarterTurns = (int)Math.Round(angle / 90);
|
||||
if (Math.Abs(angle - quarterTurns * 90) > 0.01)
|
||||
throw new ArgumentException("SurfaceTexture orientation is not a quarter turn.", nameof(textureMatrix));
|
||||
var producerRotation = (quarterTurns * 90 + 360) % 360;
|
||||
return (clockwiseDegrees - producerRotation + 360) % 360;
|
||||
return mirrored
|
||||
? (producerRotation - clockwiseDegrees + 360) % 360
|
||||
: (clockwiseDegrees - producerRotation + 360) % 360;
|
||||
}
|
||||
|
||||
private static void Validate(int degrees)
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@ internal static class PodcastPermissions
|
|||
private static TaskCompletionSource<bool>? _pending;
|
||||
private static string[] _requested = [];
|
||||
|
||||
public static async Task EnsureAsync(Activity activity, bool microphone, CancellationToken ct)
|
||||
public static async Task EnsureAsync(Activity activity, bool microphone, CancellationToken ct, bool notifications = true)
|
||||
{
|
||||
var requested = new List<string> { Manifest.Permission.Camera };
|
||||
if (microphone) requested.Add(Manifest.Permission.RecordAudio);
|
||||
if (OperatingSystem.IsAndroidVersionAtLeast(33)) requested.Add(Manifest.Permission.PostNotifications);
|
||||
if (notifications && OperatingSystem.IsAndroidVersionAtLeast(33)) requested.Add(Manifest.Permission.PostNotifications);
|
||||
var permissions = requested.ToArray();
|
||||
if (permissions.All(p => ContextCompat.CheckSelfPermission(activity, p) == Permission.Granted)) return;
|
||||
if (_pending is not null) throw new InvalidOperationException("Une demande de permissions est deja en cours.");
|
||||
|
|
@ -30,7 +30,9 @@ internal static class PodcastPermissions
|
|||
_pending = pending;
|
||||
ActivityCompat.RequestPermissions(activity, permissions, RequestCode);
|
||||
if (!await pending.Task.WaitAsync(ct))
|
||||
throw new UnauthorizedAccessException("Autorisez la camera, les notifications et, si selectionne, le microphone pour diffuser.");
|
||||
throw new UnauthorizedAccessException(notifications
|
||||
? "Autorisez la camera, les notifications et, si selectionne, le microphone pour diffuser."
|
||||
: "Autorisez la camera pour la previsualisation.");
|
||||
}
|
||||
|
||||
public static bool HandleResult(int requestCode, string[]? permissions, Permission[]? results)
|
||||
|
|
|
|||
Loading…
Reference in a new issue