Skip to content

Commit 035d9c9

Browse files
ne0rrmatrixCopilotTheCodeTraveler
authored
Fix camera package fails to build with latest Maui version (#2916)
* Improve null safety and update dependencies Enhanced null safety across CameraManager.android.cs by adding null-conditional operators (`?`) to prevent potential null reference exceptions. Updated `CameraViewPage.xaml` to bind `Picker` to `SelectedCamera.Name` for accurate UI display. Upgraded `Xamarin.AndroidX.Camera` dependencies to version 1.5.0 and added `Xamarin.AndroidX.Activity.Ktx` for improved functionality and compatibility. Adjusted `CameraProvider.android.cs` to handle null values and ensure robust camera initialization. * Improve camera binding, resolution, and recording quality Updated CameraViewPage.xaml to fix data binding issues by modifying the `ItemDisplayBinding` property of the `Picker`. Enhanced CameraManager.android.cs with the following changes: - Improved resolution selection logic in `UpdateCaptureResolution` by prioritizing higher resolution over capture rate. - Enhanced image capture quality in `StartUseCase` by setting capture mode to maximize quality and applying the resolution selector. - Added audio support and proper initialization for video recording in `PlatformStartVideoRecording`. These changes improve the overall functionality and quality of the camera features. * Update src/CommunityToolkit.Maui.Camera/CameraManager.android.cs Co-authored-by: Copilot <[email protected]> * Ignore the camera if `Camera2CameraInfo.From` returns `null` * Update Builder Patterns * Update CameraProvider.android.cs --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Brandon Minnick <[email protected]>
1 parent 86af450 commit 035d9c9

File tree

4 files changed

+64
-31
lines changed

4 files changed

+64
-31
lines changed

samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
<Picker
6565
Title="Cameras"
6666
ItemsSource="{Binding Cameras}"
67-
ItemDisplayBinding="{Binding Name}"
67+
ItemDisplayBinding="{Binding Name, x:DataType={x:Null}}"
6868
SelectedItem="{Binding SelectedCamera}" />
6969

7070
<Picker

src/CommunityToolkit.Maui.Camera/CameraManager.android.cs

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,13 @@ public async partial ValueTask UpdateCaptureResolution(Size resolution, Cancella
124124
resolutionSelector?.Dispose();
125125

126126
resolutionSelector = new ResolutionSelector.Builder()
127-
.SetAllowedResolutionMode(ResolutionSelector.PreferHigherResolutionOverCaptureRate)
127+
.SetAllowedResolutionMode(ResolutionSelector.PreferHigherResolutionOverCaptureRate)?
128128
.SetResolutionFilter(resolutionFilter)
129-
.Build();
129+
?.Build() ?? throw new InvalidOperationException("Unable to Set Resolution Filter");
130+
131+
// `.SetResolutionFilter()` should never return null
132+
// According to the Android docs, `ResolutionSelector.Builder.setResolutionFilter(ResolutionFilter)` returns a `NonNull` object
133+
// `ResolutionSelector.Builder.SetResolutionFilter(ResolutionFilter)` returning a nullable object in .NET for Android is likely a C# Binding mistake
130134

131135
if (IsInitialized)
132136
{
@@ -229,23 +233,33 @@ protected async Task StartUseCase(CancellationToken token)
229233
videoCapture?.Dispose();
230234
videoRecorder?.Dispose();
231235

232-
cameraPreview = new Preview.Builder().SetResolutionSelector(resolutionSelector).Build();
233-
cameraPreview.SetSurfaceProvider(cameraExecutor, previewView?.SurfaceProvider);
236+
cameraPreview = new Preview.Builder().SetResolutionSelector(resolutionSelector)?.Build();
237+
cameraPreview?.SetSurfaceProvider(cameraExecutor, previewView?.SurfaceProvider);
234238

235239
imageCapture = new ImageCapture.Builder()
236-
.SetCaptureMode(ImageCapture.CaptureModeMaximizeQuality)
240+
.SetCaptureMode(ImageCapture.CaptureModeMaximizeQuality)?
237241
.SetResolutionSelector(resolutionSelector)
238-
.Build();
242+
?.Build() ?? throw new InvalidOperationException("Unable to set resolution selector");
243+
244+
// `.SetResolutionFilter()` should never return null
245+
// According to the Android docs, `ResolutionSelector.Builder.SetResolutionFilter(ResolutionFilter)` returns a `NonNull` object
246+
// `ResolutionSelector.Builder.SetResolutionFilter(ResolutionFilter)` returning a nullable object in .NET for Android is likely a C# Binding mistake
247+
// https://developer.android.com/reference/androidx/camera/core/resolutionselector/ResolutionSelector.Builder#setResolutionFilter(androidx.camera.core.resolutionselector.ResolutionFilter)
239248

240249
var videoRecorderBuilder = new Recorder.Builder()
241-
.SetExecutor(cameraExecutor);
250+
.SetExecutor(cameraExecutor) ?? throw new InvalidOperationException("Unable to set video recorder executor");
251+
252+
// `.SetExecutor()` should never return null
253+
// According to the Android docs, `ResolutionSelector.Builder.setExecutor(ResolutionFilter)` returns a `NonNull` object
254+
// `ResolutionSelector.Builder.SetExecutor(ResolutionFilter)` returning a nullable object in .NET for Android is likely a C# Binding mistake
255+
// https://developer.android.com/reference/androidx/camera/video/Recorder.Builder#setExecutor(java.util.concurrent.Executor)
242256

243257
if (Quality.Highest is not null)
244258
{
245-
videoRecorderBuilder = videoRecorderBuilder.SetQualitySelector(QualitySelector.From(Quality.Highest));
259+
videoRecorderBuilder = videoRecorderBuilder?.SetQualitySelector(QualitySelector.From(Quality.Highest));
246260
}
247261

248-
videoRecorder = videoRecorderBuilder.Build();
262+
videoRecorder = videoRecorderBuilder?.Build();
249263
videoCapture = VideoCapture.WithOutput(videoRecorder);
250264

251265
await StartCameraPreview(token);
@@ -271,9 +285,9 @@ protected virtual async partial Task PlatformStartCameraPreview(CancellationToke
271285
camera = await RebindCamera(processCameraProvider, cameraView.SelectedCamera, token, cameraPreview, imageCapture, videoCapture);
272286
cameraControl = camera.CameraControl;
273287

274-
var point = previewView.MeteringPointFactory.CreatePoint(previewView.Width / 2.0f, previewView.Height / 2.0f, 0.1f);
288+
var point = previewView.MeteringPointFactory?.CreatePoint(previewView.Width / 2.0f, previewView.Height / 2.0f, 0.1f);
275289
var action = new FocusMeteringAction.Builder(point).Build();
276-
camera.CameraControl.StartFocusAndMetering(action);
290+
camera.CameraControl?.StartFocusAndMetering(action);
277291

278292
IsInitialized = true;
279293
OnLoaded.Invoke();
@@ -344,8 +358,13 @@ protected virtual async partial Task PlatformStartVideoRecording(Stream stream,
344358
var executor = ContextCompat.GetMainExecutor(context) ?? throw new CameraException($"Unable to retrieve {nameof(IExecutorService)}");
345359
videoRecording = videoRecorder
346360
.PrepareRecording(context, outputOptions)
347-
.WithAudioEnabled()
348-
.Start(executor, captureListener);
361+
?.WithAudioEnabled()
362+
.Start(executor, captureListener) ?? throw new InvalidOperationException("Unable to prepare recording");
363+
364+
// `.PrepareRecording()` should never return null
365+
// According to the Android docs, `Recorder.prepareRecording(Context, eMediaSoreOutputOptions)` returns a `NonNull` object
366+
// `Recorder.PrepareRecording(Context, eMediaSoreOutputOptions)` returning a nullable object in .NET for Android is likely a C# Binding mistake
367+
// https://developer.android.com/reference/androidx/camera/video/Recorder#prepareRecording(android.content.Context,androidx.camera.video.MediaStoreOutputOptions)
349368
}
350369

351370
protected virtual async partial Task<Stream> PlatformStopVideoRecording(CancellationToken token)
@@ -415,7 +434,9 @@ async Task<CameraSelector> EnableModes(CameraInfo selectedCamera, CancellationTo
415434
return;
416435
}
417436

418-
var extensionsManagerFuture = ExtensionsManager.GetInstanceAsync(context, cameraProviderInstance);
437+
var extensionsManagerFuture = ExtensionsManager.GetInstanceAsync(context, cameraProviderInstance)
438+
?? throw new InvalidOperationException("Unable to get listenable future for camera provider");;
439+
419440
extensionsManagerFuture.AddListener(new Runnable(() =>
420441
{
421442
var extensionsManager = (ExtensionsManager?)extensionsManagerFuture.Get();
@@ -455,10 +476,10 @@ void SetImageCaptureTargetRotation(int rotation)
455476

456477
sealed class ImageCallBack(ICameraView cameraView) : ImageCapture.OnImageCapturedCallback
457478
{
458-
public override void OnCaptureSuccess(IImageProxy image)
479+
public override void OnCaptureSuccess(IImageProxy? image)
459480
{
460481
base.OnCaptureSuccess(image);
461-
var img = image.Image;
482+
var img = image?.Image;
462483

463484
if (img is null)
464485
{
@@ -471,7 +492,7 @@ public override void OnCaptureSuccess(IImageProxy image)
471492
if (buffer is null)
472493
{
473494
cameraView.OnMediaCapturedFailed("Unable to obtain a buffer for the image plane.");
474-
image.Close();
495+
image?.Close();
475496
return;
476497
}
477498

@@ -489,7 +510,7 @@ public override void OnCaptureSuccess(IImageProxy image)
489510
}
490511
finally
491512
{
492-
image.Close();
513+
image?.Close();
493514
}
494515

495516
static Image.Plane? GetFirstPlane(Image.Plane[]? planes)
@@ -503,24 +524,26 @@ public override void OnCaptureSuccess(IImageProxy image)
503524
}
504525
}
505526

506-
public override void OnError(ImageCaptureException exception)
527+
public override void OnError(ImageCaptureException? exception)
507528
{
508529
base.OnError(exception);
509-
cameraView.OnMediaCapturedFailed(exception.Message ?? "An unknown error occurred.");
530+
cameraView.OnMediaCapturedFailed(exception?.Message ?? "An unknown error occurred.");
510531
}
511532
}
512533

513534
sealed class ResolutionFilter(Android.Util.Size size) : Object, IResolutionFilter
514535
{
515536
public Android.Util.Size TargetSize { get; set; } = size;
516537

517-
public IList<Android.Util.Size> Filter(IList<Android.Util.Size> supportedSizes, int rotationDegrees)
538+
public IList<Android.Util.Size> Filter(IList<Android.Util.Size>? supportedSizes, int rotationDegrees)
518539
{
519-
var filteredList = supportedSizes
540+
var filteredList = supportedSizes?
520541
.Where(size => size.Width <= TargetSize.Width && size.Height <= TargetSize.Height)
521542
.OrderByDescending(size => size.Width * size.Height).ToList();
522543

523-
return filteredList.Count is 0 ? supportedSizes : filteredList;
544+
return filteredList is null || filteredList.Count is 0
545+
? supportedSizes ?? []
546+
: filteredList;
524547
}
525548
}
526549

src/CommunityToolkit.Maui.Camera/CommunityToolkit.Maui.Camera.csproj

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,12 @@
5252

5353
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
5454
<!-- Required NuGet Packages -->
55-
<PackageReference Include="Xamarin.AndroidX.Camera.Camera2" Version="1.4.2.2" />
56-
<PackageReference Include="Xamarin.AndroidX.Camera.View" Version="1.4.2.2" />
57-
<PackageReference Include="Xamarin.AndroidX.Camera.Extensions" Version="1.4.2.2" />
55+
<PackageReference Include="Xamarin.AndroidX.Camera.Camera2" Version="1.5.0" />
56+
<PackageReference Include="Xamarin.AndroidX.Camera.View" Version="1.5.0" />
57+
<PackageReference Include="Xamarin.AndroidX.Camera.Extensions" Version="1.5.0" />
5858

59+
<PackageReference Include="Xamarin.AndroidX.Activity.Ktx" Version="1.11.0" />
60+
5961
<!-- Ensure Linker does not remove required libraries -->
6062
<None Include="linker.xml" Pack="true" PackagePath="build\$(PackageId).LinkerConfigurationFile.xml" />
6163
</ItemGroup>

src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ public async partial ValueTask RefreshAvailableCameras(CancellationToken token)
3030
foreach (var cameraXInfo in processCameraProvider.AvailableCameraInfos)
3131
{
3232
var camera2Info = Camera2CameraInfo.From(cameraXInfo);
33+
if (camera2Info is null)
34+
{
35+
// `Camera2CameraInfo.From` should never return `null`
36+
// According to the Android Docs, `Camera2CameraInfo.From` returns a `NonNull`
37+
// `Camera2CameraInfo.From` returning a nullable `Camera2CameraInfo` object is likely a C# binding mistake
38+
// https://developer.android.com/reference/androidx/camera/camera2/interop/Camera2CameraInfo
39+
continue;
40+
}
3341

3442
var (name, position) = cameraXInfo.LensFacing switch
3543
{
@@ -68,13 +76,13 @@ public async partial ValueTask RefreshAvailableCameras(CancellationToken token)
6876
}
6977

7078
var cameraInfo = new CameraInfo(name,
71-
camera2Info.CameraId,
79+
camera2Info.CameraId ?? throw new InvalidOperationException("Unable to retrieve Camera ID"),
7280
position,
7381
cameraXInfo.HasFlashUnit,
74-
(cameraXInfo.ZoomState.Value as IZoomState)?.MinZoomRatio ?? 1.0f,
75-
(cameraXInfo.ZoomState.Value as IZoomState)?.MaxZoomRatio ?? 1.0f,
82+
(cameraXInfo.ZoomState?.Value as IZoomState)?.MinZoomRatio ?? 1.0f,
83+
(cameraXInfo.ZoomState?.Value as IZoomState)?.MaxZoomRatio ?? 1.0f,
7684
supportedResolutions,
77-
cameraXInfo.CameraSelector);
85+
cameraXInfo.CameraSelector ?? throw new InvalidOperationException($"Unable to retrieve {nameof(ICameraInfo.CameraSelector)}"));
7886

7987
availableCameras.Add(cameraInfo);
8088
}

0 commit comments

Comments
 (0)