Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 23 additions & 18 deletions src/CommunityToolkit.Maui.Camera/CameraManager.android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ public void Dispose()
videoCapture?.Dispose();
videoCapture = null;

videoRecorder?.Dispose();
videoRecorder = null;

imageCallback?.Dispose();
imageCallback = null;

Expand Down Expand Up @@ -117,7 +120,7 @@ public NativePlatformCameraPreviewView CreatePlatformView()
}

cameraExecutor = Executors.NewSingleThreadExecutor() ?? throw new CameraException($"Unable to retrieve {nameof(IExecutorService)}");
orientationListener = new OrientationListener(SetImageCaptureTargetRotation, context);
orientationListener = new OrientationListener(OnOrientationChanged, context);
orientationListener.Enable();

return previewView;
Expand Down Expand Up @@ -292,6 +295,11 @@ private partial ValueTask PlatformTakePicture(CancellationToken token)

private async partial Task PlatformStartVideoRecording(Stream stream, CancellationToken token)
{
if (videoCapture is null || videoRecorder is null)
{
await StartUseCase(token);
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PlatformStartVideoRecording can call StartUseCase(token) to recreate imageCapture/videoCapture/cameraPreview. Because target rotation is only applied from OnOrientationChanged, newly recreated use cases will keep the default TargetRotation until the device orientation changes again (which may never happen), leading to incorrect rotation after a rebuild (e.g., after a resolution change). Consider applying an initial TargetRotation immediately after rebuilding (e.g., from previewView.Display?.Rotation / current display rotation, or cache the last computed target rotation and reapply it).

Suggested change
await StartUseCase(token);
await StartUseCase(token);
if (previewView is not null)
{
var display = previewView.Display;
if (display is not null)
{
var rotation = (int)display.Rotation;
if (imageCapture is not null)
{
imageCapture.TargetRotation = rotation;
}
if (videoCapture is not null)
{
videoCapture.TargetRotation = rotation;
}
if (cameraPreview is not null)
{
cameraPreview.TargetRotation = rotation;
}
}
}

Copilot uses AI. Check for mistakes.
}

if (previewView is null
|| processCameraProvider is null
|| cameraPreview is null
Expand Down Expand Up @@ -377,12 +385,6 @@ void CleanupVideoRecordingResources()
videoRecordingFile = null;
}

videoRecorder?.Dispose();
videoRecorder = null;

videoCapture?.Dispose();
videoCapture = null;

videoRecordingFinalizeTcs = null;
}

Expand Down Expand Up @@ -425,20 +427,23 @@ async Task<ICamera> RebindCamera(ProcessCameraProvider provider, CameraInfo came
return provider.BindToLifecycle((ILifecycleOwner)context, cameraSelector, useCases);
}

void SetImageCaptureTargetRotation(int rotation)
void OnOrientationChanged(int rotation)
{
if (imageCapture is not null)
{
imageCapture.TargetRotation = rotation switch
{
>= 45 and < 135 => (int)SurfaceOrientation.Rotation270,
>= 135 and < 225 => (int)SurfaceOrientation.Rotation180,
>= 225 and < 315 => (int)SurfaceOrientation.Rotation90,
_ => (int)SurfaceOrientation.Rotation0
};
}
var targetRotation = GetSurfaceRotation(rotation);

Comment on lines +430 to +433
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OnOrientationChanged(int rotation) parameter is receiving raw orientation degrees from OrientationEventListener, not a surface rotation value. Renaming the parameter (e.g., to orientationDegrees) would avoid confusion with the computed targetRotation (surface rotation) used for TargetRotation assignments.

Copilot uses AI. Check for mistakes.
imageCapture?.TargetRotation = targetRotation;
videoCapture?.TargetRotation = targetRotation;
cameraPreview?.TargetRotation = targetRotation;
Comment on lines +434 to +436
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OnOrientationChanged sets TargetRotation on imageCapture/videoCapture/cameraPreview, but StartUseCase disposes these use cases without first setting the fields to null. If an orientation event fires between Dispose() and the subsequent re-creation, this can end up calling TargetRotation on a disposed Java object and throw. Consider setting the fields to null immediately after disposing in StartUseCase (or temporarily disabling the OrientationListener during rebuild) so the callback never observes disposed instances.

Suggested change
imageCapture?.TargetRotation = targetRotation;
videoCapture?.TargetRotation = targetRotation;
cameraPreview?.TargetRotation = targetRotation;
try
{
if (imageCapture is not null && imageCapture.Handle != IntPtr.Zero)
{
imageCapture.TargetRotation = targetRotation;
}
if (videoCapture is not null && videoCapture.Handle != IntPtr.Zero)
{
videoCapture.TargetRotation = targetRotation;
}
if (cameraPreview is not null && cameraPreview.Handle != IntPtr.Zero)
{
cameraPreview.TargetRotation = targetRotation;
}
}
catch (ObjectDisposedException)
{
// Ignore orientation updates for disposed camera use cases.
}

Copilot uses AI. Check for mistakes.
}

static int GetSurfaceRotation(int orientationDegrees) => orientationDegrees switch
{
>= 45 and < 135 => (int)SurfaceOrientation.Rotation270,
>= 135 and < 225 => (int)SurfaceOrientation.Rotation180,
>= 225 and < 315 => (int)SurfaceOrientation.Rotation90,
_ => (int)SurfaceOrientation.Rotation0
};

sealed class ImageCallBack(ICameraView cameraView) : ImageCapture.OnImageCapturedCallback
{
public override void OnCaptureSuccess(IImageProxy image)
Expand Down
Loading