diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index c80f0884f55..40e35763735 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -22,3 +22,4 @@ * Provides LifecycleOwner implementation for Activities that use the plugin that do not implement it themselves. * Implements retrieval of camera information. * Updates README.md with plugin overview and adds contribution guide to CONTRIBUTING.md. +* Implements video capture. diff --git a/packages/camera/camera_android_camerax/android/build.gradle b/packages/camera/camera_android_camerax/android/build.gradle index d056449de87..99e13b0e331 100644 --- a/packages/camera/camera_android_camerax/android/build.gradle +++ b/packages/camera/camera_android_camerax/android/build.gradle @@ -66,6 +66,7 @@ dependencies { implementation "androidx.camera:camera-core:${camerax_version}" implementation "androidx.camera:camera-camera2:${camerax_version}" implementation "androidx.camera:camera-lifecycle:${camerax_version}" + implementation "androidx.camera:camera-video:${camerax_version}" implementation 'com.google.guava:guava:31.1-android' testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-inline:5.0.0' diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java index 0289c93cb7c..39a6c4a84a1 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java @@ -19,6 +19,9 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, ActivityAware { private InstanceManager instanceManager; private FlutterPluginBinding pluginBinding; + private PendingRecordingHostApiImpl pendingRecordingHostApiImpl; + private RecorderHostApiImpl recorderHostApiImpl; + private VideoCaptureHostApiImpl videoCaptureHostApiImpl; private ImageAnalysisHostApiImpl imageAnalysisHostApiImpl; private ImageCaptureHostApiImpl imageCaptureHostApiImpl; public SystemServicesHostApiImpl systemServicesHostApiImpl; @@ -60,7 +63,8 @@ public void setUp( new ProcessCameraProviderHostApiImpl(binaryMessenger, instanceManager, context); GeneratedCameraXLibrary.ProcessCameraProviderHostApi.setup( binaryMessenger, processCameraProviderHostApiImpl); - systemServicesHostApiImpl = new SystemServicesHostApiImpl(binaryMessenger, instanceManager); + systemServicesHostApiImpl = + new SystemServicesHostApiImpl(binaryMessenger, instanceManager, context); GeneratedCameraXLibrary.SystemServicesHostApi.setup(binaryMessenger, systemServicesHostApiImpl); GeneratedCameraXLibrary.PreviewHostApi.setup( binaryMessenger, new PreviewHostApiImpl(binaryMessenger, instanceManager, textureRegistry)); @@ -73,6 +77,16 @@ public void setUp( binaryMessenger, new AnalyzerHostApiImpl(binaryMessenger, instanceManager)); GeneratedCameraXLibrary.ImageProxyHostApi.setup( binaryMessenger, new ImageProxyHostApiImpl(binaryMessenger, instanceManager)); + GeneratedCameraXLibrary.RecordingHostApi.setup( + binaryMessenger, new RecordingHostApiImpl(binaryMessenger, instanceManager)); + recorderHostApiImpl = new RecorderHostApiImpl(binaryMessenger, instanceManager, context); + GeneratedCameraXLibrary.RecorderHostApi.setup(binaryMessenger, recorderHostApiImpl); + pendingRecordingHostApiImpl = + new PendingRecordingHostApiImpl(binaryMessenger, instanceManager, context); + GeneratedCameraXLibrary.PendingRecordingHostApi.setup( + binaryMessenger, pendingRecordingHostApiImpl); + videoCaptureHostApiImpl = new VideoCaptureHostApiImpl(binaryMessenger, instanceManager); + GeneratedCameraXLibrary.VideoCaptureHostApi.setup(binaryMessenger, videoCaptureHostApiImpl); } @Override @@ -135,6 +149,15 @@ public void updateContext(@NonNull Context context) { if (processCameraProviderHostApiImpl != null) { processCameraProviderHostApiImpl.setContext(context); } + if (recorderHostApiImpl != null) { + recorderHostApiImpl.setContext(context); + } + if (pendingRecordingHostApiImpl != null) { + pendingRecordingHostApiImpl.setContext(context); + } + if (systemServicesHostApiImpl != null) { + systemServicesHostApiImpl.setContext(context); + } if (imageCaptureHostApiImpl != null) { imageCaptureHostApiImpl.setContext(context); } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java index da025b11dac..d227cc2ffde 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java @@ -13,6 +13,7 @@ import androidx.camera.core.ImageAnalysis; import androidx.camera.core.ImageCapture; import androidx.camera.core.Preview; +import androidx.camera.video.Recorder; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionInfo; import java.io.File; @@ -62,6 +63,13 @@ public SystemServicesFlutterApiImpl createSystemServicesFlutterApiImpl( return new SystemServicesFlutterApiImpl(binaryMessenger); } + /** Creates an instance of {@link Recorder.Builder}. */ + @NonNull + public Recorder.Builder createRecorderBuilder() { + return new Recorder.Builder(); + } + + @NonNull public ImageCapture.Builder createImageCaptureBuilder() { return new ImageCapture.Builder(); } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java index 882c1d14632..05cb2c4cd06 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.camerax; @@ -966,6 +966,9 @@ void startListeningForDeviceOrientationChange( void stopListeningForDeviceOrientationChange(); + @NonNull + String getTempFilePath(@NonNull String prefix, @NonNull String suffix); + /** The codec used by SystemServicesHostApi. */ static @NonNull MessageCodec getCodec() { return SystemServicesHostApiCodec.INSTANCE; @@ -1058,6 +1061,32 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String prefixArg = (String) args.get(0); + String suffixArg = (String) args.get(1); + try { + String output = api.getTempFilePath(prefixArg, suffixArg); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } } } /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ @@ -1260,6 +1289,472 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable PreviewHos } } } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface VideoCaptureHostApi { + + @NonNull + Long withOutput(@NonNull Long videoOutputId); + + @NonNull + Long getOutput(@NonNull Long identifier); + + /** The codec used by VideoCaptureHostApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** + * Sets up an instance of `VideoCaptureHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable VideoCaptureHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.VideoCaptureHostApi.withOutput", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number videoOutputIdArg = (Number) args.get(0); + try { + Long output = + api.withOutput( + (videoOutputIdArg == null) ? null : videoOutputIdArg.longValue()); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.VideoCaptureHostApi.getOutput", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + Long output = + api.getOutput((identifierArg == null) ? null : identifierArg.longValue()); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class VideoCaptureFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public VideoCaptureFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by VideoCaptureFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + + public void create(@NonNull Long identifierArg, @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.VideoCaptureFlutterApi.create", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(identifierArg)), + channelReply -> callback.reply(null)); + } + } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface RecorderHostApi { + + void create(@NonNull Long identifier, @Nullable Long aspectRatio, @Nullable Long bitRate); + + @NonNull + Long getAspectRatio(@NonNull Long identifier); + + @NonNull + Long getTargetVideoEncodingBitRate(@NonNull Long identifier); + + @NonNull + Long prepareRecording(@NonNull Long identifier, @NonNull String path); + + /** The codec used by RecorderHostApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** + * Sets up an instance of `RecorderHostApi` to handle messages through the `binaryMessenger`. + */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable RecorderHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.RecorderHostApi.create", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + Number aspectRatioArg = (Number) args.get(1); + Number bitRateArg = (Number) args.get(2); + try { + api.create( + (identifierArg == null) ? null : identifierArg.longValue(), + (aspectRatioArg == null) ? null : aspectRatioArg.longValue(), + (bitRateArg == null) ? null : bitRateArg.longValue()); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.RecorderHostApi.getAspectRatio", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + Long output = + api.getAspectRatio( + (identifierArg == null) ? null : identifierArg.longValue()); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.RecorderHostApi.getTargetVideoEncodingBitRate", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + Long output = + api.getTargetVideoEncodingBitRate( + (identifierArg == null) ? null : identifierArg.longValue()); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.RecorderHostApi.prepareRecording", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + String pathArg = (String) args.get(1); + try { + Long output = + api.prepareRecording( + (identifierArg == null) ? null : identifierArg.longValue(), pathArg); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class RecorderFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public RecorderFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by RecorderFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + + public void create( + @NonNull Long identifierArg, + @Nullable Long aspectRatioArg, + @Nullable Long bitRateArg, + @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.RecorderFlutterApi.create", getCodec()); + channel.send( + new ArrayList(Arrays.asList(identifierArg, aspectRatioArg, bitRateArg)), + channelReply -> callback.reply(null)); + } + } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface PendingRecordingHostApi { + + @NonNull + Long start(@NonNull Long identifier); + + /** The codec used by PendingRecordingHostApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** + * Sets up an instance of `PendingRecordingHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup( + @NonNull BinaryMessenger binaryMessenger, @Nullable PendingRecordingHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PendingRecordingHostApi.start", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + Long output = + api.start((identifierArg == null) ? null : identifierArg.longValue()); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class PendingRecordingFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public PendingRecordingFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by PendingRecordingFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + + public void create(@NonNull Long identifierArg, @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PendingRecordingFlutterApi.create", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(identifierArg)), + channelReply -> callback.reply(null)); + } + } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface RecordingHostApi { + + void close(@NonNull Long identifier); + + void pause(@NonNull Long identifier); + + void resume(@NonNull Long identifier); + + void stop(@NonNull Long identifier); + + /** The codec used by RecordingHostApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** + * Sets up an instance of `RecordingHostApi` to handle messages through the `binaryMessenger`. + */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable RecordingHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.RecordingHostApi.close", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + api.close((identifierArg == null) ? null : identifierArg.longValue()); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.RecordingHostApi.pause", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + api.pause((identifierArg == null) ? null : identifierArg.longValue()); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.RecordingHostApi.resume", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + api.resume((identifierArg == null) ? null : identifierArg.longValue()); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.RecordingHostApi.stop", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + api.stop((identifierArg == null) ? null : identifierArg.longValue()); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class RecordingFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public RecordingFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by RecordingFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + + public void create(@NonNull Long identifierArg, @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.RecordingFlutterApi.create", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(identifierArg)), + channelReply -> callback.reply(null)); + } + } private static class ImageCaptureHostApiCodec extends StandardMessageCodec { public static final ImageCaptureHostApiCodec INSTANCE = new ImageCaptureHostApiCodec(); diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingFlutterApiImpl.java new file mode 100644 index 00000000000..9b4f7108056 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingFlutterApiImpl.java @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.camera.video.PendingRecording; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.PendingRecordingFlutterApi; + +public class PendingRecordingFlutterApiImpl extends PendingRecordingFlutterApi { + private final InstanceManager instanceManager; + + public PendingRecordingFlutterApiImpl( + @Nullable BinaryMessenger binaryMessenger, @Nullable InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + void create(@NonNull PendingRecording pendingRecording, @Nullable Reply reply) { + create(instanceManager.addHostCreatedInstance(pendingRecording), reply); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingHostApiImpl.java new file mode 100644 index 00000000000..70119fac5d4 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingHostApiImpl.java @@ -0,0 +1,86 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.camera.video.PendingRecording; +import androidx.camera.video.Recording; +import androidx.camera.video.VideoRecordEvent; +import androidx.core.content.ContextCompat; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.PendingRecordingHostApi; +import java.util.Objects; +import java.util.concurrent.Executor; + +public class PendingRecordingHostApiImpl implements PendingRecordingHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + private Context context; + + @VisibleForTesting @NonNull public CameraXProxy cameraXProxy = new CameraXProxy(); + + @VisibleForTesting SystemServicesFlutterApiImpl systemServicesFlutterApi; + + @VisibleForTesting RecordingFlutterApiImpl recordingFlutterApi; + + public PendingRecordingHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, + @NonNull InstanceManager instanceManager, + @Nullable Context context) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + this.context = context; + systemServicesFlutterApi = cameraXProxy.createSystemServicesFlutterApiImpl(binaryMessenger); + recordingFlutterApi = new RecordingFlutterApiImpl(binaryMessenger, instanceManager); + } + + /** Sets the context, which is used to get the {@link Executor} needed to start the recording. */ + public void setContext(@Nullable Context context) { + this.context = context; + } + + /** + * Starts the given {@link PendingRecording}, creating a new {@link Recording}. The recording is + * then added to the instance manager and we return the corresponding identifier. + * + * @param identifier An identifier corresponding to a PendingRecording. + */ + @NonNull + @Override + public Long start(@NonNull Long identifier) { + PendingRecording pendingRecording = getPendingRecordingFromInstanceId(identifier); + Recording recording = + pendingRecording.start(this.getExecutor(), event -> handleVideoRecordEvent(event)); + recordingFlutterApi.create(recording, reply -> {}); + return Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(recording)); + } + + @Nullable + @VisibleForTesting + public Executor getExecutor() { + return ContextCompat.getMainExecutor(context); + } + + /** + * Handles {@link VideoRecordEvent}s that come in during video recording. Sends any errors + * encountered using {@link SystemServicesFlutterApiImpl}. + */ + @VisibleForTesting + public void handleVideoRecordEvent(@NonNull VideoRecordEvent event) { + if (event instanceof VideoRecordEvent.Finalize) { + VideoRecordEvent.Finalize castedEvent = (VideoRecordEvent.Finalize) event; + if (castedEvent.hasError()) { + systemServicesFlutterApi.sendCameraError(castedEvent.getCause().toString(), reply -> {}); + } + } + } + + private PendingRecording getPendingRecordingFromInstanceId(Long instanceId) { + return (PendingRecording) Objects.requireNonNull(instanceManager.getInstance(instanceId)); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java index 8491fa25fd4..b014fc81788 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java @@ -83,7 +83,9 @@ public void onSurfaceRequested(SurfaceRequest request) { flutterSurface, Executors.newSingleThreadExecutor(), (result) -> { - // See https://developer.android.com/reference/androidx/camera/core/SurfaceRequest.Result for documentation. + // See + // https://developer.android.com/reference/androidx/camera/core/SurfaceRequest.Result + // for documentation. // Always attempt a release. flutterSurface.release(); int resultCode = result.getResultCode(); diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderFlutterApiImpl.java new file mode 100644 index 00000000000..5258996ca65 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderFlutterApiImpl.java @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.camera.video.Recorder; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.RecorderFlutterApi; + +public class RecorderFlutterApiImpl extends RecorderFlutterApi { + private final InstanceManager instanceManager; + + public RecorderFlutterApiImpl( + @Nullable BinaryMessenger binaryMessenger, @Nullable InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + void create( + @NonNull Recorder recorder, + @Nullable Long aspectRatio, + @Nullable Long bitRate, + @Nullable Reply reply) { + create(instanceManager.addHostCreatedInstance(recorder), aspectRatio, bitRate, reply); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderHostApiImpl.java new file mode 100644 index 00000000000..660c469c88b --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecorderHostApiImpl.java @@ -0,0 +1,113 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.camera.video.FileOutputOptions; +import androidx.camera.video.PendingRecording; +import androidx.camera.video.Recorder; +import androidx.core.content.ContextCompat; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.RecorderHostApi; +import java.io.File; +import java.util.Objects; +import java.util.concurrent.Executor; + +public class RecorderHostApiImpl implements RecorderHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + private Context context; + + @NonNull @VisibleForTesting public CameraXProxy cameraXProxy = new CameraXProxy(); + + @NonNull @VisibleForTesting public PendingRecordingFlutterApiImpl pendingRecordingFlutterApi; + + public RecorderHostApiImpl( + @Nullable BinaryMessenger binaryMessenger, + @NonNull InstanceManager instanceManager, + @Nullable Context context) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + this.context = context; + this.pendingRecordingFlutterApi = + new PendingRecordingFlutterApiImpl(binaryMessenger, instanceManager); + } + + @Override + public void create(@NonNull Long instanceId, @Nullable Long aspectRatio, @Nullable Long bitRate) { + Recorder.Builder recorderBuilder = cameraXProxy.createRecorderBuilder(); + if (aspectRatio != null) { + recorderBuilder.setAspectRatio(aspectRatio.intValue()); + } + if (bitRate != null) { + recorderBuilder.setTargetVideoEncodingBitRate(bitRate.intValue()); + } + Recorder recorder = recorderBuilder.setExecutor(ContextCompat.getMainExecutor(context)).build(); + instanceManager.addDartCreatedInstance(recorder, instanceId); + } + + /** Sets the context, which is used to get the {@link Executor} passed to the Recorder builder. */ + public void setContext(@Nullable Context context) { + this.context = context; + } + + /** Gets the aspect ratio of the given {@link Recorder}. */ + @NonNull + @Override + public Long getAspectRatio(@NonNull Long identifier) { + Recorder recorder = getRecorderFromInstanceId(identifier); + return Long.valueOf(recorder.getAspectRatio()); + } + + /** Gets the target video encoding bitrate of the given {@link Recorder}. */ + @NonNull + @Override + public Long getTargetVideoEncodingBitRate(@NonNull Long identifier) { + Recorder recorder = getRecorderFromInstanceId(identifier); + return Long.valueOf(recorder.getTargetVideoEncodingBitRate()); + } + + /** + * Uses the provided {@link Recorder} to prepare a recording that will be saved to a file at the + * provided path. + */ + @NonNull + @Override + public Long prepareRecording(@NonNull Long identifier, @NonNull String path) { + Recorder recorder = getRecorderFromInstanceId(identifier); + File temporaryCaptureFile = openTempFile(path); + FileOutputOptions fileOutputOptions = + new FileOutputOptions.Builder(temporaryCaptureFile).build(); + PendingRecording pendingRecording = recorder.prepareRecording(context, fileOutputOptions); + if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) + == PackageManager.PERMISSION_GRANTED) { + pendingRecording.withAudioEnabled(); + } + pendingRecordingFlutterApi.create(pendingRecording, reply -> {}); + return Objects.requireNonNull( + instanceManager.getIdentifierForStrongReference(pendingRecording)); + } + + @Nullable + @VisibleForTesting + public File openTempFile(@NonNull String path) { + File file = null; + try { + file = new File(path); + } catch (NullPointerException | SecurityException e) { + throw new RuntimeException(e); + } + return file; + } + + private Recorder getRecorderFromInstanceId(Long instanceId) { + return (Recorder) Objects.requireNonNull(instanceManager.getInstance(instanceId)); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecordingFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecordingFlutterApiImpl.java new file mode 100644 index 00000000000..8fe7082f95c --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecordingFlutterApiImpl.java @@ -0,0 +1,24 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import androidx.camera.video.Recording; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.RecordingFlutterApi; + +public class RecordingFlutterApiImpl extends RecordingFlutterApi { + private final InstanceManager instanceManager; + + public RecordingFlutterApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + void create(@NonNull Recording recording, Reply reply) { + create(instanceManager.addHostCreatedInstance(recording), reply); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecordingHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecordingHostApiImpl.java new file mode 100644 index 00000000000..9288464da86 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecordingHostApiImpl.java @@ -0,0 +1,50 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import androidx.camera.video.Recording; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.RecordingHostApi; +import java.util.Objects; + +public class RecordingHostApiImpl implements RecordingHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + + public RecordingHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + } + + @Override + public void close(@NonNull Long identifier) { + Recording recording = getRecordingFromInstanceId(identifier); + recording.close(); + } + + @Override + public void pause(@NonNull Long identifier) { + Recording recording = getRecordingFromInstanceId(identifier); + recording.pause(); + } + + @Override + public void resume(@NonNull Long identifier) { + Recording recording = getRecordingFromInstanceId(identifier); + recording.resume(); + } + + @Override + public void stop(@NonNull Long identifier) { + Recording recording = getRecordingFromInstanceId(identifier); + recording.stop(); + } + + private Recording getRecordingFromInstanceId(Long instanceId) { + return (Recording) Objects.requireNonNull(instanceManager.getInstance(instanceId)); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java index a6985811531..ffd387e181e 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java @@ -5,17 +5,23 @@ package io.flutter.plugins.camerax; import android.app.Activity; +import android.content.Context; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camerax.CameraPermissionsManager.PermissionsRegistry; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraPermissionsErrorData; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.Result; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesFlutterApi; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesHostApi; +import java.io.File; +import java.io.IOException; public class SystemServicesHostApiImpl implements SystemServicesHostApi { private final BinaryMessenger binaryMessenger; private final InstanceManager instanceManager; + private Context context; @VisibleForTesting public CameraXProxy cameraXProxy = new CameraXProxy(); @VisibleForTesting public DeviceOrientationManager deviceOrientationManager; @@ -25,12 +31,18 @@ public class SystemServicesHostApiImpl implements SystemServicesHostApi { private PermissionsRegistry permissionsRegistry; public SystemServicesHostApiImpl( - BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + BinaryMessenger binaryMessenger, InstanceManager instanceManager, Context context) { this.binaryMessenger = binaryMessenger; this.instanceManager = instanceManager; + this.context = context; this.systemServicesFlutterApi = new SystemServicesFlutterApiImpl(binaryMessenger); } + /** Sets the context, which is used to get the cache directory. */ + public void setContext(Context context) { + this.context = context; + } + public void setActivity(Activity activity) { this.activity = activity; } @@ -70,7 +82,7 @@ public void requestCameraPermissions( } /** - * Starts listening for device orientation changes using an instace of a {@link + * Starts listening for device orientation changes using an instance of a {@link * DeviceOrientationManager}. * *

Whenever a change in device orientation is detected by the {@code DeviceOrientationManager}, @@ -78,7 +90,7 @@ public void requestCameraPermissions( */ @Override public void startListeningForDeviceOrientationChange( - Boolean isFrontFacing, Long sensorOrientation) { + @NonNull Boolean isFrontFacing, @NonNull Long sensorOrientation) { deviceOrientationManager = cameraXProxy.createDeviceOrientationManager( activity, @@ -108,4 +120,19 @@ public void stopListeningForDeviceOrientationChange() { deviceOrientationManager.stop(); } } + + /** Returns a path to be used to create a temp file in the current cache directory. */ + @Override + @NonNull + public String getTempFilePath(@NonNull String prefix, @NonNull String suffix) { + try { + File path = File.createTempFile(prefix, suffix, context.getCacheDir()); + return path.toString(); + } catch (IOException | SecurityException e) { + throw new GeneratedCameraXLibrary.FlutterError( + "getTempFilePath_failure", + "SystemServicesHostApiImpl.getTempFilePath encountered an exception: " + e.toString(), + null); + } + } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoCaptureFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoCaptureFlutterApiImpl.java new file mode 100644 index 00000000000..3e316fa5774 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoCaptureFlutterApiImpl.java @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import androidx.camera.video.Recorder; +import androidx.camera.video.VideoCapture; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.VideoCaptureFlutterApi; + +public class VideoCaptureFlutterApiImpl extends VideoCaptureFlutterApi { + public VideoCaptureFlutterApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + private final InstanceManager instanceManager; + + void create(@NonNull VideoCapture videoCapture, Reply reply) { + create(instanceManager.addHostCreatedInstance(videoCapture), reply); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoCaptureHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoCaptureHostApiImpl.java new file mode 100644 index 00000000000..7e764cdff4a --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoCaptureHostApiImpl.java @@ -0,0 +1,52 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.camera.video.Recorder; +import androidx.camera.video.VideoCapture; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.VideoCaptureHostApi; +import java.util.Objects; + +public class VideoCaptureHostApiImpl implements VideoCaptureHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + + public VideoCaptureHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + } + + @Override + @NonNull + public Long withOutput(@NonNull Long videoOutputId) { + Recorder recorder = + (Recorder) Objects.requireNonNull(instanceManager.getInstance(videoOutputId)); + VideoCapture videoCapture = VideoCapture.withOutput(recorder); + final VideoCaptureFlutterApiImpl videoCaptureFlutterApi = + getVideoCaptureFlutterApiImpl(binaryMessenger, instanceManager); + videoCaptureFlutterApi.create(videoCapture, result -> {}); + return Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(videoCapture)); + } + + @Override + @NonNull + public Long getOutput(@NonNull Long identifier) { + VideoCapture videoCapture = + Objects.requireNonNull(instanceManager.getInstance(identifier)); + Recorder recorder = videoCapture.getOutput(); + return Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(recorder)); + } + + @VisibleForTesting + @NonNull + public VideoCaptureFlutterApiImpl getVideoCaptureFlutterApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + return new VideoCaptureFlutterApiImpl(binaryMessenger, instanceManager); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PendingRecordingTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PendingRecordingTest.java new file mode 100644 index 00000000000..92415d5381a --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PendingRecordingTest.java @@ -0,0 +1,107 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import androidx.camera.video.PendingRecording; +import androidx.camera.video.Recording; +import androidx.camera.video.VideoRecordEvent; +import io.flutter.plugin.common.BinaryMessenger; +import java.util.Objects; +import java.util.concurrent.Executor; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class PendingRecordingTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public PendingRecording mockPendingRecording; + @Mock public Recording mockRecording; + @Mock public RecordingFlutterApiImpl mockRecordingFlutterApi; + @Mock public Context mockContext; + @Mock public SystemServicesFlutterApiImpl mockSystemServicesFlutterApi; + @Mock public VideoRecordEvent.Finalize event; + @Mock public Throwable throwable; + + InstanceManager testInstanceManager; + + @Before + public void setUp() { + testInstanceManager = spy(InstanceManager.create(identifier -> {})); + } + + @After + public void tearDown() { + testInstanceManager.stopFinalizationListener(); + } + + @Test + public void testStart() { + final Long mockPendingRecordingId = 3L; + final Long mockRecordingId = testInstanceManager.addHostCreatedInstance(mockRecording); + testInstanceManager.addDartCreatedInstance(mockPendingRecording, mockPendingRecordingId); + + doReturn(mockRecording).when(mockPendingRecording).start(any(), any()); + doNothing().when(mockRecordingFlutterApi).create(any(Recording.class), any()); + PendingRecordingHostApiImpl spy = + spy(new PendingRecordingHostApiImpl(mockBinaryMessenger, testInstanceManager, mockContext)); + doReturn(mock(Executor.class)).when(spy).getExecutor(); + spy.recordingFlutterApi = mockRecordingFlutterApi; + assertEquals(spy.start(mockPendingRecordingId), mockRecordingId); + verify(mockRecordingFlutterApi).create(eq(mockRecording), any()); + + testInstanceManager.remove(mockPendingRecordingId); + testInstanceManager.remove(mockRecordingId); + } + + @Test + public void testHandleVideoRecordEventSendsError() { + PendingRecordingHostApiImpl pendingRecordingHostApi = + new PendingRecordingHostApiImpl(mockBinaryMessenger, testInstanceManager, mockContext); + pendingRecordingHostApi.systemServicesFlutterApi = mockSystemServicesFlutterApi; + final String eventMessage = "example failure message"; + + when(event.hasError()).thenReturn(true); + when(event.getCause()).thenReturn(throwable); + when(throwable.toString()).thenReturn(eventMessage); + doNothing().when(mockSystemServicesFlutterApi).sendCameraError(any(), any()); + + pendingRecordingHostApi.handleVideoRecordEvent(event); + + verify(mockSystemServicesFlutterApi).sendCameraError(eq(eventMessage), any()); + } + + @Test + public void flutterApiCreateTest() { + final PendingRecordingFlutterApiImpl spyPendingRecordingFlutterApi = + spy(new PendingRecordingFlutterApiImpl(mockBinaryMessenger, testInstanceManager)); + + spyPendingRecordingFlutterApi.create(mockPendingRecording, reply -> {}); + + final long identifier = + Objects.requireNonNull( + testInstanceManager.getIdentifierForStrongReference(mockPendingRecording)); + verify(spyPendingRecordingFlutterApi).create(eq(identifier), any()); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java index abf99257368..39b73abd738 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java @@ -148,10 +148,12 @@ public void createSurfaceProvider_createsExpectedPreviewSurfaceProvider() { verify(mockSurfaceRequest) .provideSurface(surfaceCaptor.capture(), any(Executor.class), consumerCaptor.capture()); - // Test that the surface derived from the surface texture entry will be provided to the surface request. + // Test that the surface derived from the surface texture entry will be provided to the surface + // request. assertEquals(surfaceCaptor.getValue(), mockSurface); - // Test that the Consumer used to handle surface request result releases Flutter surface texture appropriately + // Test that the Consumer used to handle surface request result releases Flutter surface texture + // appropriately // and sends camera errors appropriately. Consumer capturedConsumer = consumerCaptor.getValue(); diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/RecorderTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/RecorderTest.java new file mode 100644 index 00000000000..cf41949165a --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/RecorderTest.java @@ -0,0 +1,170 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import androidx.camera.video.FileOutputOptions; +import androidx.camera.video.PendingRecording; +import androidx.camera.video.Recorder; +import androidx.test.core.app.ApplicationProvider; +import io.flutter.plugin.common.BinaryMessenger; +import java.io.File; +import java.util.Objects; +import java.util.concurrent.Executor; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class RecorderTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public Recorder mockRecorder; + private Context context; + + InstanceManager testInstanceManager; + + @Before + public void setUp() { + testInstanceManager = spy(InstanceManager.create(identifier -> {})); + context = ApplicationProvider.getApplicationContext(); + } + + @After + public void tearDown() { + testInstanceManager.stopFinalizationListener(); + } + + @Test + public void createTest() { + final int recorderId = 0; + final int aspectRatio = 1; + final int bitRate = 2; + + final RecorderHostApiImpl recorderHostApi = + new RecorderHostApiImpl(mockBinaryMessenger, testInstanceManager, context); + + final CameraXProxy mockCameraXProxy = mock(CameraXProxy.class); + final Recorder.Builder mockRecorderBuilder = mock(Recorder.Builder.class); + recorderHostApi.cameraXProxy = mockCameraXProxy; + when(mockCameraXProxy.createRecorderBuilder()).thenReturn(mockRecorderBuilder); + when(mockRecorderBuilder.setAspectRatio(aspectRatio)).thenReturn(mockRecorderBuilder); + when(mockRecorderBuilder.setTargetVideoEncodingBitRate(bitRate)) + .thenReturn(mockRecorderBuilder); + when(mockRecorderBuilder.setExecutor(any(Executor.class))).thenReturn(mockRecorderBuilder); + when(mockRecorderBuilder.build()).thenReturn(mockRecorder); + + recorderHostApi.create( + Long.valueOf(recorderId), Long.valueOf(aspectRatio), Long.valueOf(bitRate)); + verify(mockCameraXProxy).createRecorderBuilder(); + verify(mockRecorderBuilder).setAspectRatio(aspectRatio); + verify(mockRecorderBuilder).setTargetVideoEncodingBitRate(bitRate); + verify(mockRecorderBuilder).build(); + assertEquals(testInstanceManager.getInstance(Long.valueOf(recorderId)), mockRecorder); + testInstanceManager.remove(Long.valueOf(recorderId)); + } + + @Test + public void getAspectRatioTest() { + final int recorderId = 3; + final int aspectRatio = 6; + + when(mockRecorder.getAspectRatio()).thenReturn(aspectRatio); + testInstanceManager.addDartCreatedInstance(mockRecorder, Long.valueOf(recorderId)); + final RecorderHostApiImpl recorderHostApi = + new RecorderHostApiImpl(mockBinaryMessenger, testInstanceManager, context); + assertEquals( + recorderHostApi.getAspectRatio(Long.valueOf(recorderId)), Long.valueOf(aspectRatio)); + verify(mockRecorder).getAspectRatio(); + testInstanceManager.remove(Long.valueOf(recorderId)); + } + + @Test + public void getTargetVideoEncodingBitRateTest() { + final int bitRate = 7; + final int recorderId = 3; + + when(mockRecorder.getTargetVideoEncodingBitRate()).thenReturn(bitRate); + testInstanceManager.addDartCreatedInstance(mockRecorder, Long.valueOf(recorderId)); + final RecorderHostApiImpl recorderHostApi = + new RecorderHostApiImpl(mockBinaryMessenger, testInstanceManager, context); + assertEquals( + recorderHostApi.getTargetVideoEncodingBitRate(Long.valueOf(recorderId)), + Long.valueOf(bitRate)); + verify(mockRecorder).getTargetVideoEncodingBitRate(); + testInstanceManager.remove(Long.valueOf(recorderId)); + } + + @Test + @SuppressWarnings("unchecked") + public void prepareRecording_returnsExpectedPendingRecording() { + final int recorderId = 3; + + PendingRecordingFlutterApiImpl mockPendingRecordingFlutterApi = + mock(PendingRecordingFlutterApiImpl.class); + PendingRecording mockPendingRecording = mock(PendingRecording.class); + testInstanceManager.addDartCreatedInstance(mockRecorder, Long.valueOf(recorderId)); + when(mockRecorder.prepareRecording(any(Context.class), any(FileOutputOptions.class))) + .thenReturn(mockPendingRecording); + when(mockPendingRecording.withAudioEnabled()).thenReturn(mockPendingRecording); + doNothing().when(mockPendingRecordingFlutterApi).create(any(PendingRecording.class), any()); + Long mockPendingRecordingId = testInstanceManager.addHostCreatedInstance(mockPendingRecording); + + RecorderHostApiImpl spy = + spy(new RecorderHostApiImpl(mockBinaryMessenger, testInstanceManager, context)); + spy.pendingRecordingFlutterApi = mockPendingRecordingFlutterApi; + doReturn(mock(File.class)).when(spy).openTempFile(any()); + spy.prepareRecording(Long.valueOf(recorderId), ""); + + testInstanceManager.remove(Long.valueOf(recorderId)); + testInstanceManager.remove(mockPendingRecordingId); + } + + @Test + @SuppressWarnings("unchecked") + public void prepareRecording_errorsWhenPassedNullPath() { + final int recorderId = 3; + + testInstanceManager.addDartCreatedInstance(mockRecorder, Long.valueOf(recorderId)); + RecorderHostApiImpl recorderHostApi = + new RecorderHostApiImpl(mockBinaryMessenger, testInstanceManager, context); + assertThrows( + RuntimeException.class, + () -> { + recorderHostApi.prepareRecording(Long.valueOf(recorderId), null); + }); + testInstanceManager.remove(Long.valueOf(recorderId)); + } + + @Test + public void flutterApiCreateTest() { + final RecorderFlutterApiImpl spyRecorderFlutterApi = + spy(new RecorderFlutterApiImpl(mockBinaryMessenger, testInstanceManager)); + + spyRecorderFlutterApi.create(mockRecorder, null, null, reply -> {}); + + final long identifier = + Objects.requireNonNull(testInstanceManager.getIdentifierForStrongReference(mockRecorder)); + verify(spyRecorderFlutterApi).create(eq(identifier), eq(null), eq(null), any()); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/RecordingTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/RecordingTest.java new file mode 100644 index 00000000000..ca0c9964956 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/RecordingTest.java @@ -0,0 +1,111 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import androidx.camera.video.Recording; +import io.flutter.plugin.common.BinaryMessenger; +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class RecordingTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public Recording mockRecording; + + InstanceManager testInstanceManager; + + @Before + public void setUp() { + testInstanceManager = spy(InstanceManager.create(identifier -> {})); + } + + @After + public void tearDown() { + testInstanceManager.stopFinalizationListener(); + } + + @Test + public void close_getsRecordingFromInstanceManagerAndCloses() { + final RecordingHostApiImpl recordingHostApi = + new RecordingHostApiImpl(mockBinaryMessenger, testInstanceManager); + final Long recordingId = 5L; + + testInstanceManager.addDartCreatedInstance(mockRecording, recordingId); + + recordingHostApi.close(recordingId); + + verify(mockRecording).close(); + testInstanceManager.remove(recordingId); + } + + @Test + public void stop_getsRecordingFromInstanceManagerAndStops() { + final RecordingHostApiImpl recordingHostApi = + new RecordingHostApiImpl(mockBinaryMessenger, testInstanceManager); + final Long recordingId = 5L; + + testInstanceManager.addDartCreatedInstance(mockRecording, recordingId); + + recordingHostApi.stop(recordingId); + + verify(mockRecording).stop(); + testInstanceManager.remove(recordingId); + } + + @Test + public void resume_getsRecordingFromInstanceManagerAndResumes() { + final RecordingHostApiImpl recordingHostApi = + new RecordingHostApiImpl(mockBinaryMessenger, testInstanceManager); + final Long recordingId = 5L; + + testInstanceManager.addDartCreatedInstance(mockRecording, recordingId); + + recordingHostApi.resume(recordingId); + + verify(mockRecording).resume(); + testInstanceManager.remove(recordingId); + } + + @Test + public void pause_getsRecordingFromInstanceManagerAndPauses() { + final RecordingHostApiImpl recordingHostApi = + new RecordingHostApiImpl(mockBinaryMessenger, testInstanceManager); + final Long recordingId = 5L; + + testInstanceManager.addDartCreatedInstance(mockRecording, recordingId); + + recordingHostApi.pause(recordingId); + + verify(mockRecording).pause(); + testInstanceManager.remove(recordingId); + } + + @Test + public void flutterApiCreateTest() { + final RecordingFlutterApiImpl spyRecordingFlutterApi = + spy(new RecordingFlutterApiImpl(mockBinaryMessenger, testInstanceManager)); + + spyRecordingFlutterApi.create(mockRecording, reply -> {}); + + final long identifier = + Objects.requireNonNull(testInstanceManager.getIdentifierForStrongReference(mockRecording)); + verify(spyRecordingFlutterApi).create(eq(identifier), any()); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java index 562fbe8b526..f905704cbc1 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java @@ -5,14 +5,17 @@ package io.flutter.plugins.camerax; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Activity; +import android.content.Context; import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camerax.CameraPermissionsManager.PermissionsRegistry; @@ -21,11 +24,14 @@ import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraPermissionsErrorData; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.Result; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesFlutterApi.Reply; +import java.io.File; +import java.io.IOException; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -34,11 +40,12 @@ public class SystemServicesTest { @Mock public BinaryMessenger mockBinaryMessenger; @Mock public InstanceManager mockInstanceManager; + @Mock public Context mockContext; @Test public void requestCameraPermissionsTest() { final SystemServicesHostApiImpl systemServicesHostApi = - new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager); + new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext); final CameraXProxy mockCameraXProxy = mock(CameraXProxy.class); final CameraPermissionsManager mockCameraPermissionsManager = mock(CameraPermissionsManager.class); @@ -92,7 +99,7 @@ public void requestCameraPermissionsTest() { @Test public void deviceOrientationChangeTest() { final SystemServicesHostApiImpl systemServicesHostApi = - new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager); + new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext); final CameraXProxy mockCameraXProxy = mock(CameraXProxy.class); final Activity mockActivity = mock(Activity.class); final DeviceOrientationManager mockDeviceOrientationManager = @@ -137,4 +144,44 @@ public void deviceOrientationChangeTest() { // Test that the DeviceOrientationManager starts listening for device orientation changes. verify(mockDeviceOrientationManager).start(); } + + @Test + public void getTempFilePath_returnsCorrectPath() { + final SystemServicesHostApiImpl systemServicesHostApi = + new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext); + + final String prefix = "prefix"; + final String suffix = ".suffix"; + final MockedStatic mockedStaticFile = mockStatic(File.class); + final File mockOutputDir = mock(File.class); + final File mockFile = mock(File.class); + when(mockContext.getCacheDir()).thenReturn(mockOutputDir); + mockedStaticFile + .when(() -> File.createTempFile(prefix, suffix, mockOutputDir)) + .thenReturn(mockFile); + when(mockFile.toString()).thenReturn(prefix + suffix); + assertEquals(systemServicesHostApi.getTempFilePath(prefix, suffix), prefix + suffix); + + mockedStaticFile.close(); + } + + @Test + public void getTempFilePath_throwsRuntimeExceptionOnIOException() { + final SystemServicesHostApiImpl systemServicesHostApi = + new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext); + + final String prefix = "prefix"; + final String suffix = ".suffix"; + final MockedStatic mockedStaticFile = mockStatic(File.class); + final File mockOutputDir = mock(File.class); + when(mockContext.getCacheDir()).thenReturn(mockOutputDir); + mockedStaticFile + .when(() -> File.createTempFile(prefix, suffix, mockOutputDir)) + .thenThrow(IOException.class); + assertThrows( + GeneratedCameraXLibrary.FlutterError.class, + () -> systemServicesHostApi.getTempFilePath(prefix, suffix)); + + mockedStaticFile.close(); + } } diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/VideoCaptureTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/VideoCaptureTest.java new file mode 100644 index 00000000000..aa5656a8db4 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/VideoCaptureTest.java @@ -0,0 +1,102 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import androidx.camera.video.Recorder; +import androidx.camera.video.VideoCapture; +import io.flutter.plugin.common.BinaryMessenger; +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class VideoCaptureTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public Recorder mockRecorder; + @Mock public VideoCaptureFlutterApiImpl mockVideoCaptureFlutterApi; + @Mock public VideoCapture mockVideoCapture; + + InstanceManager testInstanceManager; + + @Before + public void setUp() { + testInstanceManager = spy(InstanceManager.create(identifier -> {})); + } + + @After + public void tearDown() { + testInstanceManager.stopFinalizationListener(); + } + + @Test + public void getOutput_returnsAssociatedRecorder() { + final Long recorderId = 5L; + final Long videoCaptureId = 6L; + VideoCapture videoCapture = VideoCapture.withOutput(mockRecorder); + + testInstanceManager.addDartCreatedInstance(mockRecorder, recorderId); + testInstanceManager.addDartCreatedInstance(videoCapture, videoCaptureId); + + VideoCaptureHostApiImpl videoCaptureHostApi = + new VideoCaptureHostApiImpl(mockBinaryMessenger, testInstanceManager); + assertEquals(videoCaptureHostApi.getOutput(videoCaptureId), recorderId); + testInstanceManager.remove(recorderId); + testInstanceManager.remove(videoCaptureId); + } + + @Test + @SuppressWarnings("unchecked") + public void withOutput_returnsNewVideoCaptureWithAssociatedRecorder() { + final Long recorderId = 5L; + testInstanceManager.addDartCreatedInstance(mockRecorder, recorderId); + + VideoCaptureHostApiImpl videoCaptureHostApi = + new VideoCaptureHostApiImpl(mockBinaryMessenger, testInstanceManager); + VideoCaptureHostApiImpl spyVideoCaptureApi = spy(videoCaptureHostApi); + doReturn(mockVideoCaptureFlutterApi) + .when(spyVideoCaptureApi) + .getVideoCaptureFlutterApiImpl(mockBinaryMessenger, testInstanceManager); + doNothing() + .when(mockVideoCaptureFlutterApi) + .create( + any(VideoCapture.class), + any(GeneratedCameraXLibrary.VideoCaptureFlutterApi.Reply.class)); + final Long videoCaptureId = videoCaptureHostApi.withOutput(recorderId); + VideoCapture videoCapture = testInstanceManager.getInstance(videoCaptureId); + assertEquals(videoCapture.getOutput(), mockRecorder); + + testInstanceManager.remove(recorderId); + testInstanceManager.remove(videoCaptureId); + } + + @Test + public void flutterApiCreateTest() { + final VideoCaptureFlutterApiImpl spyVideoCaptureFlutterApi = + spy(new VideoCaptureFlutterApiImpl(mockBinaryMessenger, testInstanceManager)); + spyVideoCaptureFlutterApi.create(mockVideoCapture, reply -> {}); + + final long identifier = + Objects.requireNonNull( + testInstanceManager.getIdentifierForStrongReference(mockVideoCapture)); + verify(spyVideoCaptureFlutterApi).create(eq(identifier), any()); + } +} diff --git a/packages/camera/camera_android_camerax/example/lib/main.dart b/packages/camera/camera_android_camerax/example/lib/main.dart index c668871b7a6..16ba8639a67 100644 --- a/packages/camera/camera_android_camerax/example/lib/main.dart +++ b/packages/camera/camera_android_camerax/example/lib/main.dart @@ -514,7 +514,8 @@ class _CameraExampleHomeState extends State IconButton( icon: const Icon(Icons.videocam), color: Colors.blue, - onPressed: () {}, // TODO(camsim99): Add functionality back here. + onPressed: + cameraController == null ? null : onVideoRecordButtonPressed, ), IconButton( icon: cameraController != null && @@ -522,12 +523,20 @@ class _CameraExampleHomeState extends State ? const Icon(Icons.play_arrow) : const Icon(Icons.pause), color: Colors.blue, - onPressed: () {}, // TODO(camsim99): Add functionality back here. + onPressed: () { + if (cameraController == null) { + return; + } else if (cameraController.value.isRecordingPaused) { + return onResumeButtonPressed(); + } else { + return onPauseButtonPressed(); + } + }, ), IconButton( icon: const Icon(Icons.stop), color: Colors.red, - onPressed: () {}, // TODO(camsim99): Add functionality back here. + onPressed: cameraController == null ? null : onStopButtonPressed, ), IconButton( icon: const Icon(Icons.pause_presentation), diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart index 174a373e3b9..6fdaa7e182c 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart @@ -17,12 +17,16 @@ import 'exposure_state.dart'; import 'image_analysis.dart'; import 'image_capture.dart'; import 'image_proxy.dart'; +import 'pending_recording.dart'; import 'plane_proxy.dart'; import 'preview.dart'; import 'process_camera_provider.dart'; +import 'recorder.dart'; +import 'recording.dart'; import 'surface.dart'; import 'system_services.dart'; import 'use_case.dart'; +import 'video_capture.dart'; import 'zoom_state.dart'; /// The Android implementation of [CameraPlatform] that uses the CameraX library. @@ -49,8 +53,33 @@ class AndroidCameraCameraX extends CameraPlatform { @visibleForTesting Preview? preview; + /// The [VideoCapture] instance that can be instantiated and configured to + /// handle video recording + @visibleForTesting + VideoCapture? videoCapture; + + /// The [Recorder] instance handling the current creating a new [PendingRecording]. + @visibleForTesting + Recorder? recorder; + + /// The [PendingRecording] instance used to create an active [Recording]. + @visibleForTesting + PendingRecording? pendingRecording; + + /// The [Recording] instance representing the current recording. + @visibleForTesting + Recording? recording; + + /// The path at which the video file will be saved for the current [Recording]. + @visibleForTesting + String? videoOutputPath; + bool _previewIsPaused = false; + /// The prefix used to create the filename for video recording files. + @visibleForTesting + final String videoPrefix = 'MOV'; + /// The [ImageCapture] instance that can be configured to capture a still image. @visibleForTesting ImageCapture? imageCapture; @@ -149,7 +178,7 @@ class AndroidCameraCameraX extends CameraPlatform { /// /// In the CameraX library, cameras are accessed by combining [UseCase]s /// to an instance of a [ProcessCameraProvider]. Thus, to create an - /// unitialized camera instance, this method retrieves a + /// uninitialized camera instance, this method retrieves a /// [ProcessCameraProvider] instance. /// /// To return the camera ID, which is equivalent to the ID of the surface texture @@ -192,8 +221,14 @@ class AndroidCameraCameraX extends CameraPlatform { _getTargetResolutionForImageCapture(_resolutionPreset); imageCapture = createImageCapture(null, imageCaptureTargetResolution); + // Configure VideoCapture and Recorder instances. + // TODO(gmackall): Enable video capture resolution configuration in createRecorder(). + recorder = createRecorder(); + videoCapture = await createVideoCapture(recorder!); + // Bind configured UseCases to ProcessCameraProvider instance & mark Preview - // instance as bound but not paused. + // instance as bound but not paused. Video capture is bound at first use + // instead of here. camera = await processCameraProvider! .bindToLifecycle(cameraSelector!, [preview!, imageCapture!]); cameraInfo = await camera!.getCameraInfo(); @@ -379,6 +414,78 @@ class AndroidCameraCameraX extends CameraPlatform { return XFile(picturePath); } + /// Configures and starts a video recording. Returns silently without doing + /// anything if there is currently an active recording. + @override + Future startVideoRecording(int cameraId, + {Duration? maxVideoDuration}) async { + assert(cameraSelector != null); + assert(processCameraProvider != null); + + if (recording != null) { + // There is currently an active recording, so do not start a new one. + return; + } + + if (!(await processCameraProvider!.isBound(videoCapture!))) { + camera = await processCameraProvider! + .bindToLifecycle(cameraSelector!, [videoCapture!]); + } + + videoOutputPath = + await SystemServices.getTempFilePath(videoPrefix, '.temp'); + pendingRecording = await recorder!.prepareRecording(videoOutputPath!); + recording = await pendingRecording!.start(); + } + + /// Stops the video recording and returns the file where it was saved. + /// Throws a CameraException if the recording is currently null, or if the + /// videoOutputPath is null. + /// + /// If the videoOutputPath is null the recording objects are cleaned up + /// so starting a new recording is possible. + @override + Future stopVideoRecording(int cameraId) async { + if (recording == null) { + throw CameraException( + 'videoRecordingFailed', + 'Attempting to stop a ' + 'video recording while no recording is in progress.'); + } + if (videoOutputPath == null) { + // Stop the current active recording as we will be unable to complete it + // in this error case. + recording!.close(); + recording = null; + pendingRecording = null; + throw CameraException( + 'INVALID_PATH', + 'The platform did not return a path ' + 'while reporting success. The platform should always ' + 'return a valid path or report an error.'); + } + recording!.close(); + recording = null; + pendingRecording = null; + return XFile(videoOutputPath!); + } + + /// Pause the current video recording if it is not null. + @override + Future pauseVideoRecording(int cameraId) async { + if (recording != null) { + recording!.pause(); + } + } + + /// Resume the current video recording if it is not null. + @override + Future resumeVideoRecording(int cameraId) async { + if (recording != null) { + recording!.resume(); + } + } + /// A new streamed frame is available. /// /// Listening to this stream will start streaming, and canceling will stop. @@ -605,6 +712,18 @@ class AndroidCameraCameraX extends CameraPlatform { targetFlashMode: flashMode, targetResolution: targetResolution); } + /// Returns a [Recorder] for use in video capture. + @visibleForTesting + Recorder createRecorder() { + return Recorder(); + } + + /// Returns a [VideoCapture] associated with the provided [Recorder]. + @visibleForTesting + Future createVideoCapture(Recorder recorder) async { + return VideoCapture.withOutput(recorder); + } + /// Returns an [ImageAnalysis] configured with specified target resolution. @visibleForTesting ImageAnalysis createImageAnalysis(ResolutionInfo? targetResolution) { diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart index c5692ae105f..408555654b0 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart @@ -10,9 +10,13 @@ import 'camerax_library.g.dart'; import 'exposure_state.dart'; import 'image_proxy.dart'; import 'java_object.dart'; +import 'pending_recording.dart'; import 'plane_proxy.dart'; import 'process_camera_provider.dart'; +import 'recorder.dart'; +import 'recording.dart'; import 'system_services.dart'; +import 'video_capture.dart'; import 'zoom_state.dart'; /// Handles initialization of Flutter APIs for the Android CameraX library. @@ -25,6 +29,10 @@ class AndroidCameraXCameraFlutterApis { CameraSelectorFlutterApiImpl? cameraSelectorFlutterApi, ProcessCameraProviderFlutterApiImpl? processCameraProviderFlutterApi, SystemServicesFlutterApiImpl? systemServicesFlutterApi, + PendingRecordingFlutterApiImpl? pendingRecordingFlutterApiImpl, + RecordingFlutterApiImpl? recordingFlutterApiImpl, + RecorderFlutterApiImpl? recorderFlutterApiImpl, + VideoCaptureFlutterApiImpl? videoCaptureFlutterApiImpl, ExposureStateFlutterApiImpl? exposureStateFlutterApiImpl, ZoomStateFlutterApiImpl? zoomStateFlutterApiImpl, AnalyzerFlutterApiImpl? analyzerFlutterApiImpl, @@ -42,6 +50,14 @@ class AndroidCameraXCameraFlutterApis { this.cameraFlutterApi = cameraFlutterApi ?? CameraFlutterApiImpl(); this.systemServicesFlutterApi = systemServicesFlutterApi ?? SystemServicesFlutterApiImpl(); + this.pendingRecordingFlutterApiImpl = + pendingRecordingFlutterApiImpl ?? PendingRecordingFlutterApiImpl(); + this.recordingFlutterApiImpl = + recordingFlutterApiImpl ?? RecordingFlutterApiImpl(); + this.recorderFlutterApiImpl = + recorderFlutterApiImpl ?? RecorderFlutterApiImpl(); + this.videoCaptureFlutterApiImpl = + videoCaptureFlutterApiImpl ?? VideoCaptureFlutterApiImpl(); this.exposureStateFlutterApiImpl = exposureStateFlutterApiImpl ?? ExposureStateFlutterApiImpl(); this.zoomStateFlutterApiImpl = @@ -81,6 +97,18 @@ class AndroidCameraXCameraFlutterApis { /// Flutter Api for [SystemServices]. late final SystemServicesFlutterApiImpl systemServicesFlutterApi; + /// Flutter Api for [PendingRecording]. + late final PendingRecordingFlutterApiImpl pendingRecordingFlutterApiImpl; + + /// Flutter Api for [Recording]. + late final RecordingFlutterApiImpl recordingFlutterApiImpl; + + /// Flutter Api for [Recorder]. + late final RecorderFlutterApiImpl recorderFlutterApiImpl; + + /// Flutter Api for [VideoCapture]. + late final VideoCaptureFlutterApiImpl videoCaptureFlutterApiImpl; + /// Flutter Api for [ExposureState]. late final ExposureStateFlutterApiImpl exposureStateFlutterApiImpl; @@ -105,6 +133,10 @@ class AndroidCameraXCameraFlutterApis { ProcessCameraProviderFlutterApi.setup(processCameraProviderFlutterApi); CameraFlutterApi.setup(cameraFlutterApi); SystemServicesFlutterApi.setup(systemServicesFlutterApi); + PendingRecordingFlutterApi.setup(pendingRecordingFlutterApiImpl); + RecordingFlutterApi.setup(recordingFlutterApiImpl); + RecorderFlutterApi.setup(recorderFlutterApiImpl); + VideoCaptureFlutterApi.setup(videoCaptureFlutterApiImpl); ExposureStateFlutterApi.setup(exposureStateFlutterApiImpl); ZoomStateFlutterApi.setup(zoomStateFlutterApiImpl); AnalyzerFlutterApi.setup(analyzerFlutterApiImpl); diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart index f2d2e17152f..8609549d0b9 100644 --- a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart +++ b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import @@ -765,6 +765,33 @@ class SystemServicesHostApi { return; } } + + Future getTempFilePath(String arg_prefix, String arg_suffix) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_prefix, arg_suffix]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as String?)!; + } + } } abstract class SystemServicesFlutterApi { @@ -956,6 +983,443 @@ class PreviewHostApi { } } +class VideoCaptureHostApi { + /// Constructor for [VideoCaptureHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + VideoCaptureHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + Future withOutput(int arg_videoOutputId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoCaptureHostApi.withOutput', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_videoOutputId]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as int?)!; + } + } + + Future getOutput(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoCaptureHostApi.getOutput', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as int?)!; + } + } +} + +abstract class VideoCaptureFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + void create(int identifier); + + static void setup(VideoCaptureFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoCaptureFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoCaptureFlutterApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.VideoCaptureFlutterApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return; + }); + } + } + } +} + +class RecorderHostApi { + /// Constructor for [RecorderHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + RecorderHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + Future create( + int arg_identifier, int? arg_aspectRatio, int? arg_bitRate) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecorderHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_identifier, arg_aspectRatio, arg_bitRate]) + as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future getAspectRatio(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecorderHostApi.getAspectRatio', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as int?)!; + } + } + + Future getTargetVideoEncodingBitRate(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecorderHostApi.getTargetVideoEncodingBitRate', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as int?)!; + } + } + + Future prepareRecording(int arg_identifier, String arg_path) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecorderHostApi.prepareRecording', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_identifier, arg_path]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as int?)!; + } + } +} + +abstract class RecorderFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + void create(int identifier, int? aspectRatio, int? bitRate); + + static void setup(RecorderFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecorderFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.RecorderFlutterApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.RecorderFlutterApi.create was null, expected non-null int.'); + final int? arg_aspectRatio = (args[1] as int?); + final int? arg_bitRate = (args[2] as int?); + api.create(arg_identifier!, arg_aspectRatio, arg_bitRate); + return; + }); + } + } + } +} + +class PendingRecordingHostApi { + /// Constructor for [PendingRecordingHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + PendingRecordingHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + Future start(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PendingRecordingHostApi.start', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as int?)!; + } + } +} + +abstract class PendingRecordingFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + void create(int identifier); + + static void setup(PendingRecordingFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PendingRecordingFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PendingRecordingFlutterApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.PendingRecordingFlutterApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return; + }); + } + } + } +} + +class RecordingHostApi { + /// Constructor for [RecordingHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + RecordingHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + Future close(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecordingHostApi.close', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future pause(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecordingHostApi.pause', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future resume(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecordingHostApi.resume', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future stop(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecordingHostApi.stop', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } +} + +abstract class RecordingFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + void create(int identifier); + + static void setup(RecordingFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecordingFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.RecordingFlutterApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.RecordingFlutterApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return; + }); + } + } + } +} + class _ImageCaptureHostApiCodec extends StandardMessageCodec { const _ImageCaptureHostApiCodec(); @override diff --git a/packages/camera/camera_android_camerax/lib/src/pending_recording.dart b/packages/camera/camera_android_camerax/lib/src/pending_recording.dart new file mode 100644 index 00000000000..179eaf85529 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/pending_recording.dart @@ -0,0 +1,101 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart' show BinaryMessenger; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; +import 'recording.dart'; + +/// Dart wrapping of PendingRecording CameraX class. +/// +/// See https://developer.android.com/reference/androidx/camera/video/PendingRecording +class PendingRecording extends JavaObject { + /// Creates a [PendingRecording] that is not automatically attached to + /// a native object. + PendingRecording.detached( + {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = PendingRecordingHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } + + late final PendingRecordingHostApiImpl _api; + + /// Starts the recording, making it an active recording. + Future start() { + return _api.startFromInstance(this); + } +} + +/// Host API implementation of [PendingRecording]. +class PendingRecordingHostApiImpl extends PendingRecordingHostApi { + /// Constructs a PendingRecordingHostApiImpl. + PendingRecordingHostApiImpl( + {this.binaryMessenger, InstanceManager? instanceManager}) + : super(binaryMessenger: binaryMessenger) { + this.instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + } + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + late final InstanceManager instanceManager; + + /// Starts the recording, making it an active recording. + Future startFromInstance(PendingRecording pendingRecording) async { + int? instanceId = instanceManager.getIdentifier(pendingRecording); + instanceId ??= instanceManager.addDartCreatedInstance(pendingRecording, + onCopy: (PendingRecording original) { + return PendingRecording.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + }); + return instanceManager + .getInstanceWithWeakReference(await start(instanceId))! as Recording; + } +} + +/// Flutter API implementation of [PendingRecording]. +class PendingRecordingFlutterApiImpl extends PendingRecordingFlutterApi { + /// Constructs a [PendingRecordingFlutterApiImpl]. + PendingRecordingFlutterApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + @override + void create(int identifier) { + instanceManager.addHostCreatedInstance( + PendingRecording.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + identifier, onCopy: (PendingRecording original) { + return PendingRecording.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + }); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/recorder.dart b/packages/camera/camera_android_camerax/lib/src/recorder.dart new file mode 100644 index 00000000000..016d7f776f8 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/recorder.dart @@ -0,0 +1,137 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; +import 'pending_recording.dart'; + +/// A dart wrapping of the CameraX Recorder class. +/// +/// See https://developer.android.com/reference/androidx/camera/video/Recorder. +class Recorder extends JavaObject { + /// Creates a [Recorder]. + Recorder( + {BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + this.aspectRatio, + this.bitRate}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + _api = RecorderHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + _api.createFromInstance(this, aspectRatio, bitRate); + } + + /// Creates a [Recorder] that is not automatically attached to a native object + Recorder.detached( + {BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + this.aspectRatio, + this.bitRate}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = RecorderHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } + + late final RecorderHostApiImpl _api; + + /// The video aspect ratio of this Recorder. + final int? aspectRatio; + + /// The intended video encoding bitrate for recording. + final int? bitRate; + + /// Prepare a recording that will be saved to a file. + Future prepareRecording(String path) { + return _api.prepareRecordingFromInstance(this, path); + } +} + +/// Host API implementation of [Recorder]. +class RecorderHostApiImpl extends RecorderHostApi { + /// Constructs a [RecorderHostApiImpl]. + RecorderHostApiImpl( + {this.binaryMessenger, InstanceManager? instanceManager}) { + this.instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + } + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + late final InstanceManager instanceManager; + + /// Creates a [Recorder] with the provided aspect ratio and bitrate if specified. + void createFromInstance(Recorder instance, int? aspectRatio, int? bitRate) { + int? identifier = instanceManager.getIdentifier(instance); + identifier ??= instanceManager.addDartCreatedInstance(instance, + onCopy: (Recorder original) { + return Recorder.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + aspectRatio: aspectRatio, + bitRate: bitRate); + }); + create(identifier, aspectRatio, bitRate); + } + + /// Prepares a [Recording] using this recorder. The output file will be saved + /// at the provided path. + Future prepareRecordingFromInstance( + Recorder instance, String path) async { + final int pendingRecordingId = + await prepareRecording(instanceManager.getIdentifier(instance)!, path); + + return instanceManager.getInstanceWithWeakReference(pendingRecordingId)!; + } +} + +/// Flutter API implementation of [Recorder]. +class RecorderFlutterApiImpl extends RecorderFlutterApi { + /// Constructs a [RecorderFlutterApiImpl]. + RecorderFlutterApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + @override + void create(int identifier, int? aspectRatio, int? bitRate) { + instanceManager.addHostCreatedInstance( + Recorder.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + aspectRatio: aspectRatio, + bitRate: bitRate, + ), + identifier, onCopy: (Recorder original) { + return Recorder.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + aspectRatio: aspectRatio, + bitRate: bitRate, + ); + }); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/recording.dart b/packages/camera/camera_android_camerax/lib/src/recording.dart new file mode 100644 index 00000000000..2f21e255b62 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/recording.dart @@ -0,0 +1,120 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart' show BinaryMessenger; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; + +/// Wraps a CameraX recording class. +/// +/// See https://developer.android.com/reference/androidx/camera/video/Recording. +class Recording extends JavaObject { + /// Constructs a detached [Recording] + Recording.detached( + {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ) { + _api = RecordingHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } + + late final RecordingHostApiImpl _api; + + /// Closes this recording. + Future close() { + return _api.closeFromInstance(this); + } + + /// Pauses this recording if active. + Future pause() { + return _api.pauseFromInstance(this); + } + + /// Resumes the current recording if paused. + Future resume() { + return _api.resumeFromInstance(this); + } + + /// Stops the recording, as if calling close(). + Future stop() { + return _api.stopFromInstance(this); + } +} + +/// Host API implementation of [Recording]. +class RecordingHostApiImpl extends RecordingHostApi { + /// Creates a [RecordingHostApiImpl]. + RecordingHostApiImpl({this.binaryMessenger, InstanceManager? instanceManager}) + : super(binaryMessenger: binaryMessenger) { + this.instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + } + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + late final InstanceManager instanceManager; + + /// Closes the specified recording instance. + Future closeFromInstance(Recording recording) async { + close(instanceManager.getIdentifier(recording)!); + } + + /// Pauses the specified recording instance if active. + Future pauseFromInstance(Recording recording) async { + pause(instanceManager.getIdentifier(recording)!); + } + + /// Resumes the specified recording instance if paused. + Future resumeFromInstance(Recording recording) async { + resume(instanceManager.getIdentifier(recording)!); + } + + /// Stops the specified recording instance, as if calling closeFromInstance(). + Future stopFromInstance(Recording recording) async { + stop(instanceManager.getIdentifier(recording)!); + } +} + +/// Flutter API implementation of [Recording]. +class RecordingFlutterApiImpl extends RecordingFlutterApi { + /// Constructs a [RecordingFlutterApiImpl]. + RecordingFlutterApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + @override + void create(int identifier) { + instanceManager.addHostCreatedInstance( + Recording.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + identifier, onCopy: (Recording original) { + return Recording.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + }); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/system_services.dart b/packages/camera/camera_android_camerax/lib/src/system_services.dart index e108b6140be..64567531d02 100644 --- a/packages/camera/camera_android_camerax/lib/src/system_services.dart +++ b/packages/camera/camera_android_camerax/lib/src/system_services.dart @@ -61,6 +61,25 @@ class SystemServices { api.stopListeningForDeviceOrientationChange(); } + + /// Returns a file path which was used to create a temporary file. + /// Prefix is a part of the file name, and suffix is the file extension. + /// + /// The file and path constraints are determined by the implementation of + /// File.createTempFile(prefix, suffix, cacheDir), on the android side, where + /// where cacheDir is the cache directory identified by the current application + /// context using context.getCacheDir(). + /// + /// Ex: getTempFilePath('prefix', 'suffix') would return a string of the form + /// '/prefix3213453.suffix', where the numbers after prefix and + /// before suffix are determined by the call to File.createTempFile and + /// therefore random. + static Future getTempFilePath(String prefix, String suffix, + {BinaryMessenger? binaryMessenger}) { + final SystemServicesHostApi api = + SystemServicesHostApi(binaryMessenger: binaryMessenger); + return api.getTempFilePath(prefix, suffix); + } } /// Host API implementation of [SystemServices]. diff --git a/packages/camera/camera_android_camerax/lib/src/video_capture.dart b/packages/camera/camera_android_camerax/lib/src/video_capture.dart new file mode 100644 index 00000000000..0c624d159ed --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/video_capture.dart @@ -0,0 +1,116 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; +import 'recorder.dart'; +import 'use_case.dart'; + +/// Dart wrapping of CameraX VideoCapture class. +/// +/// See https://developer.android.com/reference/androidx/camera/video/VideoCapture. +class VideoCapture extends UseCase { + /// Creates a VideoCapture that is not automatically attached to a native object. + VideoCapture.detached( + {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = VideoCaptureHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } + + /// Creates a [VideoCapture] associated with the given [Recorder]. + static Future withOutput(Recorder recorder, + {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager}) { + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + final VideoCaptureHostApiImpl api = VideoCaptureHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + + return api.withOutputFromInstance(recorder); + } + + /// Gets the [Recorder] associated with this VideoCapture. + Future getOutput() { + return _api.getOutputFromInstance(this); + } + + late final VideoCaptureHostApiImpl _api; +} + +/// Host API implementation of [VideoCapture]. +class VideoCaptureHostApiImpl extends VideoCaptureHostApi { + /// Constructs a [VideoCaptureHostApiImpl]. + VideoCaptureHostApiImpl( + {this.binaryMessenger, InstanceManager? instanceManager}) { + this.instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + } + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + late final InstanceManager instanceManager; + + /// Creates a [VideoCapture] associated with the provided [Recorder] instance. + Future withOutputFromInstance(Recorder recorder) async { + int? identifier = instanceManager.getIdentifier(recorder); + identifier ??= instanceManager.addDartCreatedInstance(recorder, + onCopy: (Recorder original) { + return Recorder( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + }); + final int videoCaptureId = await withOutput(identifier); + return instanceManager + .getInstanceWithWeakReference(videoCaptureId)!; + } + + /// Gets the [Recorder] associated with the provided [VideoCapture] instance. + Future getOutputFromInstance(VideoCapture instance) async { + final int? identifier = instanceManager.getIdentifier(instance); + final int recorderId = await getOutput(identifier!); + return instanceManager.getInstanceWithWeakReference(recorderId)!; + } +} + +/// Flutter API implementation of [VideoCapture]. +class VideoCaptureFlutterApiImpl implements VideoCaptureFlutterApi { + /// Constructs a [VideoCaptureFlutterApiImpl]. + VideoCaptureFlutterApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + @override + void create(int identifier) { + instanceManager.addHostCreatedInstance( + VideoCapture.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + identifier, onCopy: (VideoCapture original) { + return VideoCapture.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ); + }); + } +} diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart index c8b3885d064..b8e2325a216 100644 --- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart +++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart @@ -141,6 +141,8 @@ abstract class SystemServicesHostApi { bool isFrontFacing, int sensorOrientation); void stopListeningForDeviceOrientationChange(); + + String getTempFilePath(String prefix, String suffix); } @FlutterApi() @@ -161,6 +163,60 @@ abstract class PreviewHostApi { ResolutionInfo getResolutionInfo(int identifier); } +@HostApi(dartHostTestHandler: 'TestVideoCaptureHostApi') +abstract class VideoCaptureHostApi { + int withOutput(int videoOutputId); + + int getOutput(int identifier); +} + +@FlutterApi() +abstract class VideoCaptureFlutterApi { + void create(int identifier); +} + +@HostApi(dartHostTestHandler: 'TestRecorderHostApi') +abstract class RecorderHostApi { + void create(int identifier, int? aspectRatio, int? bitRate); + + int getAspectRatio(int identifier); + + int getTargetVideoEncodingBitRate(int identifier); + + int prepareRecording(int identifier, String path); +} + +@FlutterApi() +abstract class RecorderFlutterApi { + void create(int identifier, int? aspectRatio, int? bitRate); +} + +@HostApi(dartHostTestHandler: 'TestPendingRecordingHostApi') +abstract class PendingRecordingHostApi { + int start(int identifier); +} + +@FlutterApi() +abstract class PendingRecordingFlutterApi { + void create(int identifier); +} + +@HostApi(dartHostTestHandler: 'TestRecordingHostApi') +abstract class RecordingHostApi { + void close(int identifier); + + void pause(int identifier); + + void resume(int identifier); + + void stop(int identifier); +} + +@FlutterApi() +abstract class RecordingFlutterApi { + void create(int identifier); +} + @HostApi(dartHostTestHandler: 'TestImageCaptureHostApi') abstract class ImageCaptureHostApi { void create(int identifier, int? flashMode, ResolutionInfo? targetResolution); diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart index e7a6707d6e3..aa8bc81656b 100644 --- a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart @@ -15,11 +15,15 @@ import 'package:camera_android_camerax/src/exposure_state.dart'; import 'package:camera_android_camerax/src/image_analysis.dart'; import 'package:camera_android_camerax/src/image_capture.dart'; import 'package:camera_android_camerax/src/image_proxy.dart'; +import 'package:camera_android_camerax/src/pending_recording.dart'; import 'package:camera_android_camerax/src/plane_proxy.dart'; import 'package:camera_android_camerax/src/preview.dart'; import 'package:camera_android_camerax/src/process_camera_provider.dart'; +import 'package:camera_android_camerax/src/recorder.dart'; +import 'package:camera_android_camerax/src/recording.dart'; import 'package:camera_android_camerax/src/system_services.dart'; import 'package:camera_android_camerax/src/use_case.dart'; +import 'package:camera_android_camerax/src/video_capture.dart'; import 'package:camera_android_camerax/src/zoom_state.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/services.dart' show DeviceOrientation, Uint8List; @@ -43,10 +47,15 @@ import 'test_camerax_library.g.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), MockSpec(), + MockSpec(), MockSpec(), ]) -@GenerateMocks([BuildContext]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -157,6 +166,10 @@ void main() { // Verify the camera's ImageCapture instance is instantiated properly. expect(camera.imageCapture, equals(camera.testImageCapture)); + // Verify the camera's Recorder and VideoCapture instances are instantiated properly. + expect(camera.recorder, equals(camera.testRecorder)); + expect(camera.videoCapture, equals(camera.testVideoCapture)); + // Verify the camera's Preview instance has its surface provider set. verify(camera.preview!.setSurfaceProvider()); }); @@ -225,7 +238,7 @@ void main() { ResolutionInfo(width: resolutionWidth, height: resolutionHeight); // TODO(camsim99): Modify this when camera configuration is supported and - // defualt values no longer being used. + // default values no longer being used. // https://github.com/flutter/flutter/issues/120468 // https://github.com/flutter/flutter/issues/120467 final CameraInitializedEvent testCameraInitializedEvent = @@ -471,6 +484,183 @@ void main() { expect(previewTexture.textureId, equals(textureId)); }); + group('video recording', () { + test( + 'startVideoRecording binds video capture use case and starts the recording', + () async { + //Set up mocks and constants. + final MockAndroidCameraCameraX camera = MockAndroidCameraCameraX(); + camera.processCameraProvider = MockProcessCameraProvider(); + camera.cameraSelector = MockCameraSelector(); + camera.recorder = camera.testRecorder; + camera.videoCapture = camera.testVideoCapture; + camera.camera = MockCamera(); + final MockPendingRecording mockPendingRecording = MockPendingRecording(); + final MockRecording mockRecording = MockRecording(); + final TestSystemServicesHostApi mockSystemServicesApi = + MockTestSystemServicesHostApi(); + TestSystemServicesHostApi.setup(mockSystemServicesApi); + + const int cameraId = 17; + const String outputPath = '/temp/MOV123.temp'; + + // Mock method calls. + when(mockSystemServicesApi.getTempFilePath(camera.videoPrefix, '.temp')) + .thenReturn(outputPath); + when(camera.testRecorder.prepareRecording(outputPath)) + .thenAnswer((_) async => mockPendingRecording); + when(mockPendingRecording.start()).thenAnswer((_) async => mockRecording); + when(camera.processCameraProvider!.isBound(camera.videoCapture!)) + .thenAnswer((_) async => false); + when(camera.processCameraProvider!.bindToLifecycle( + camera.cameraSelector!, [camera.videoCapture!])) + .thenAnswer((_) async => camera.camera!); + + await camera.startVideoRecording(cameraId); + + verify(camera.processCameraProvider!.bindToLifecycle( + camera.cameraSelector!, [camera.videoCapture!])); + expect(camera.pendingRecording, equals(mockPendingRecording)); + expect(camera.recording, mockRecording); + }); + + test( + 'startVideoRecording binds video capture use case and starts the recording' + ' on first call, and does nothing on second call', () async { + //Set up mocks and constants. + final MockAndroidCameraCameraX camera = MockAndroidCameraCameraX(); + camera.processCameraProvider = MockProcessCameraProvider(); + camera.cameraSelector = MockCameraSelector(); + camera.recorder = camera.testRecorder; + camera.videoCapture = camera.testVideoCapture; + camera.camera = MockCamera(); + final MockPendingRecording mockPendingRecording = MockPendingRecording(); + final MockRecording mockRecording = MockRecording(); + final TestSystemServicesHostApi mockSystemServicesApi = + MockTestSystemServicesHostApi(); + TestSystemServicesHostApi.setup(mockSystemServicesApi); + + const int cameraId = 17; + const String outputPath = '/temp/MOV123.temp'; + + // Mock method calls. + when(mockSystemServicesApi.getTempFilePath(camera.videoPrefix, '.temp')) + .thenReturn(outputPath); + when(camera.testRecorder.prepareRecording(outputPath)) + .thenAnswer((_) async => mockPendingRecording); + when(mockPendingRecording.start()).thenAnswer((_) async => mockRecording); + when(camera.processCameraProvider!.isBound(camera.videoCapture!)) + .thenAnswer((_) async => false); + when(camera.processCameraProvider!.bindToLifecycle( + camera.cameraSelector!, [camera.videoCapture!])) + .thenAnswer((_) async => camera.camera!); + + await camera.startVideoRecording(cameraId); + + verify(camera.processCameraProvider!.bindToLifecycle( + camera.cameraSelector!, [camera.videoCapture!])); + expect(camera.pendingRecording, equals(mockPendingRecording)); + expect(camera.recording, mockRecording); + + await camera.startVideoRecording(cameraId); + // Verify that each of these calls happened only once. + verify(mockSystemServicesApi.getTempFilePath(camera.videoPrefix, '.temp')) + .called(1); + verifyNoMoreInteractions(mockSystemServicesApi); + verify(camera.testRecorder.prepareRecording(outputPath)).called(1); + verifyNoMoreInteractions(camera.testRecorder); + verify(mockPendingRecording.start()).called(1); + verifyNoMoreInteractions(mockPendingRecording); + }); + + test('pauseVideoRecording pauses the recording', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockRecording recording = MockRecording(); + camera.recording = recording; + camera.pauseVideoRecording(0); + verify(recording.pause()); + verifyNoMoreInteractions(recording); + }); + + test('resumeVideoRecording resumes the recording', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockRecording recording = MockRecording(); + camera.recording = recording; + camera.resumeVideoRecording(0); + verify(recording.resume()); + verifyNoMoreInteractions(recording); + }); + + test('stopVideoRecording stops the recording', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockRecording recording = MockRecording(); + final MockProcessCameraProvider processCameraProvider = + MockProcessCameraProvider(); + final MockVideoCapture videoCapture = MockVideoCapture(); + const String videoOutputPath = '/test/output/path'; + + camera.processCameraProvider = processCameraProvider; + camera.recording = recording; + camera.videoCapture = videoCapture; + camera.videoOutputPath = videoOutputPath; + + final XFile file = await camera.stopVideoRecording(0); + expect(file.path, videoOutputPath); + + verify(recording.close()); + verifyNoMoreInteractions(recording); + }); + + test( + 'stopVideoRecording throws a camera exception if ' + 'no recording is in progress', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const String videoOutputPath = '/test/output/path'; + + camera.recording = null; + camera.videoOutputPath = videoOutputPath; + + expect( + () => camera.stopVideoRecording(0), throwsA(isA())); + }); + + test( + 'stopVideoRecording throws a camera exception if ' + 'videoOutputPath is null, and sets recording to null', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockRecording recording = MockRecording(); + + camera.recording = recording; + camera.videoOutputPath = null; + + expect( + () => camera.stopVideoRecording(0), throwsA(isA())); + expect(camera.recording, null); + }); + + test( + 'calling stopVideoRecording twice stops the recording ' + 'and then throws a CameraException', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockRecording recording = MockRecording(); + final MockProcessCameraProvider processCameraProvider = + MockProcessCameraProvider(); + final MockVideoCapture videoCapture = MockVideoCapture(); + const String videoOutputPath = '/test/output/path'; + + camera.processCameraProvider = processCameraProvider; + camera.recording = recording; + camera.videoCapture = videoCapture; + camera.videoOutputPath = videoOutputPath; + + final XFile file = await camera.stopVideoRecording(0); + expect(file.path, videoOutputPath); + + expect( + () => camera.stopVideoRecording(0), throwsA(isA())); + }); + }); + test( 'takePicture binds and unbinds ImageCapture to lifecycle and makes call to take a picture', () async { @@ -712,6 +902,8 @@ class MockAndroidCameraCameraX extends AndroidCameraCameraX { final MockImageCapture testImageCapture = MockImageCapture(); final MockCameraSelector mockBackCameraSelector = MockCameraSelector(); final MockCameraSelector mockFrontCameraSelector = MockCameraSelector(); + final MockRecorder testRecorder = MockRecorder(); + final MockVideoCapture testVideoCapture = MockVideoCapture(); final MockImageAnalysis mockImageAnalysis = MockImageAnalysis(); @override @@ -748,6 +940,16 @@ class MockAndroidCameraCameraX extends AndroidCameraCameraX { return testImageCapture; } + @override + Recorder createRecorder() { + return testRecorder; + } + + @override + Future createVideoCapture(Recorder recorder) { + return Future.value(testVideoCapture); + } + @override ImageAnalysis createImageAnalysis(ResolutionInfo? targetResolution) { return mockImageAnalysis; diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart index b7365a2e90d..2fea464aa97 100644 --- a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart @@ -3,32 +3,36 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i11; -import 'dart:typed_data' as _i18; +import 'dart:async' as _i14; +import 'dart:typed_data' as _i21; -import 'package:camera_android_camerax/src/analyzer.dart' as _i14; +import 'package:camera_android_camerax/src/analyzer.dart' as _i17; import 'package:camera_android_camerax/src/camera.dart' as _i7; import 'package:camera_android_camerax/src/camera_info.dart' as _i2; -import 'package:camera_android_camerax/src/camera_selector.dart' as _i12; +import 'package:camera_android_camerax/src/camera_selector.dart' as _i15; import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i6; import 'package:camera_android_camerax/src/exposure_state.dart' as _i3; -import 'package:camera_android_camerax/src/image_analysis.dart' as _i13; -import 'package:camera_android_camerax/src/image_capture.dart' as _i15; -import 'package:camera_android_camerax/src/image_proxy.dart' as _i16; -import 'package:camera_android_camerax/src/plane_proxy.dart' as _i17; -import 'package:camera_android_camerax/src/preview.dart' as _i19; +import 'package:camera_android_camerax/src/image_analysis.dart' as _i16; +import 'package:camera_android_camerax/src/image_capture.dart' as _i18; +import 'package:camera_android_camerax/src/image_proxy.dart' as _i19; +import 'package:camera_android_camerax/src/pending_recording.dart' as _i8; +import 'package:camera_android_camerax/src/plane_proxy.dart' as _i20; +import 'package:camera_android_camerax/src/preview.dart' as _i22; import 'package:camera_android_camerax/src/process_camera_provider.dart' - as _i20; -import 'package:camera_android_camerax/src/use_case.dart' as _i21; + as _i23; +import 'package:camera_android_camerax/src/recorder.dart' as _i10; +import 'package:camera_android_camerax/src/recording.dart' as _i9; +import 'package:camera_android_camerax/src/use_case.dart' as _i24; +import 'package:camera_android_camerax/src/video_capture.dart' as _i25; import 'package:camera_android_camerax/src/zoom_state.dart' as _i4; import 'package:camera_platform_interface/camera_platform_interface.dart' as _i5; -import 'package:flutter/foundation.dart' as _i10; -import 'package:flutter/services.dart' as _i9; -import 'package:flutter/widgets.dart' as _i8; +import 'package:flutter/foundation.dart' as _i13; +import 'package:flutter/services.dart' as _i12; +import 'package:flutter/widgets.dart' as _i11; import 'package:mockito/mockito.dart' as _i1; -import 'test_camerax_library.g.dart' as _i22; +import 'test_camerax_library.g.dart' as _i26; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -114,8 +118,39 @@ class _FakeCamera_6 extends _i1.SmartFake implements _i7.Camera { ); } -class _FakeWidget_7 extends _i1.SmartFake implements _i8.Widget { - _FakeWidget_7( +class _FakePendingRecording_7 extends _i1.SmartFake + implements _i8.PendingRecording { + _FakePendingRecording_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeRecording_8 extends _i1.SmartFake implements _i9.Recording { + _FakeRecording_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeRecorder_9 extends _i1.SmartFake implements _i10.Recorder { + _FakeRecorder_9( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWidget_10 extends _i1.SmartFake implements _i11.Widget { + _FakeWidget_10( Object parent, Invocation parentInvocation, ) : super( @@ -124,13 +159,14 @@ class _FakeWidget_7 extends _i1.SmartFake implements _i8.Widget { ); @override - String toString({_i9.DiagnosticLevel? minLevel = _i9.DiagnosticLevel.info}) => + String toString( + {_i12.DiagnosticLevel? minLevel = _i12.DiagnosticLevel.info}) => super.toString(); } -class _FakeInheritedWidget_8 extends _i1.SmartFake - implements _i8.InheritedWidget { - _FakeInheritedWidget_8( +class _FakeInheritedWidget_11 extends _i1.SmartFake + implements _i11.InheritedWidget { + _FakeInheritedWidget_11( Object parent, Invocation parentInvocation, ) : super( @@ -139,13 +175,14 @@ class _FakeInheritedWidget_8 extends _i1.SmartFake ); @override - String toString({_i9.DiagnosticLevel? minLevel = _i9.DiagnosticLevel.info}) => + String toString( + {_i12.DiagnosticLevel? minLevel = _i12.DiagnosticLevel.info}) => super.toString(); } -class _FakeDiagnosticsNode_9 extends _i1.SmartFake - implements _i10.DiagnosticsNode { - _FakeDiagnosticsNode_9( +class _FakeDiagnosticsNode_12 extends _i1.SmartFake + implements _i13.DiagnosticsNode { + _FakeDiagnosticsNode_12( Object parent, Invocation parentInvocation, ) : super( @@ -155,8 +192,8 @@ class _FakeDiagnosticsNode_9 extends _i1.SmartFake @override String toString({ - _i10.TextTreeConfiguration? parentConfiguration, - _i9.DiagnosticLevel? minLevel = _i9.DiagnosticLevel.info, + _i13.TextTreeConfiguration? parentConfiguration, + _i12.DiagnosticLevel? minLevel = _i12.DiagnosticLevel.info, }) => super.toString(); } @@ -166,12 +203,12 @@ class _FakeDiagnosticsNode_9 extends _i1.SmartFake /// See the documentation for Mockito's code generation for more information. class MockCamera extends _i1.Mock implements _i7.Camera { @override - _i11.Future<_i2.CameraInfo> getCameraInfo() => (super.noSuchMethod( + _i14.Future<_i2.CameraInfo> getCameraInfo() => (super.noSuchMethod( Invocation.method( #getCameraInfo, [], ), - returnValue: _i11.Future<_i2.CameraInfo>.value(_FakeCameraInfo_0( + returnValue: _i14.Future<_i2.CameraInfo>.value(_FakeCameraInfo_0( this, Invocation.method( #getCameraInfo, @@ -179,14 +216,14 @@ class MockCamera extends _i1.Mock implements _i7.Camera { ), )), returnValueForMissingStub: - _i11.Future<_i2.CameraInfo>.value(_FakeCameraInfo_0( + _i14.Future<_i2.CameraInfo>.value(_FakeCameraInfo_0( this, Invocation.method( #getCameraInfo, [], ), )), - ) as _i11.Future<_i2.CameraInfo>); + ) as _i14.Future<_i2.CameraInfo>); } /// A class which mocks [CameraInfo]. @@ -194,21 +231,21 @@ class MockCamera extends _i1.Mock implements _i7.Camera { /// See the documentation for Mockito's code generation for more information. class MockCameraInfo extends _i1.Mock implements _i2.CameraInfo { @override - _i11.Future getSensorRotationDegrees() => (super.noSuchMethod( + _i14.Future getSensorRotationDegrees() => (super.noSuchMethod( Invocation.method( #getSensorRotationDegrees, [], ), - returnValue: _i11.Future.value(0), - returnValueForMissingStub: _i11.Future.value(0), - ) as _i11.Future); + returnValue: _i14.Future.value(0), + returnValueForMissingStub: _i14.Future.value(0), + ) as _i14.Future); @override - _i11.Future<_i3.ExposureState> getExposureState() => (super.noSuchMethod( + _i14.Future<_i3.ExposureState> getExposureState() => (super.noSuchMethod( Invocation.method( #getExposureState, [], ), - returnValue: _i11.Future<_i3.ExposureState>.value(_FakeExposureState_1( + returnValue: _i14.Future<_i3.ExposureState>.value(_FakeExposureState_1( this, Invocation.method( #getExposureState, @@ -216,21 +253,21 @@ class MockCameraInfo extends _i1.Mock implements _i2.CameraInfo { ), )), returnValueForMissingStub: - _i11.Future<_i3.ExposureState>.value(_FakeExposureState_1( + _i14.Future<_i3.ExposureState>.value(_FakeExposureState_1( this, Invocation.method( #getExposureState, [], ), )), - ) as _i11.Future<_i3.ExposureState>); + ) as _i14.Future<_i3.ExposureState>); @override - _i11.Future<_i4.ZoomState> getZoomState() => (super.noSuchMethod( + _i14.Future<_i4.ZoomState> getZoomState() => (super.noSuchMethod( Invocation.method( #getZoomState, [], ), - returnValue: _i11.Future<_i4.ZoomState>.value(_FakeZoomState_2( + returnValue: _i14.Future<_i4.ZoomState>.value(_FakeZoomState_2( this, Invocation.method( #getZoomState, @@ -238,14 +275,14 @@ class MockCameraInfo extends _i1.Mock implements _i2.CameraInfo { ), )), returnValueForMissingStub: - _i11.Future<_i4.ZoomState>.value(_FakeZoomState_2( + _i14.Future<_i4.ZoomState>.value(_FakeZoomState_2( this, Invocation.method( #getZoomState, [], ), )), - ) as _i11.Future<_i4.ZoomState>); + ) as _i14.Future<_i4.ZoomState>); } /// A class which mocks [CameraImageData]. @@ -288,19 +325,19 @@ class MockCameraImageData extends _i1.Mock implements _i5.CameraImageData { /// A class which mocks [CameraSelector]. /// /// See the documentation for Mockito's code generation for more information. -class MockCameraSelector extends _i1.Mock implements _i12.CameraSelector { +class MockCameraSelector extends _i1.Mock implements _i15.CameraSelector { @override - _i11.Future> filter(List<_i2.CameraInfo>? cameraInfos) => + _i14.Future> filter(List<_i2.CameraInfo>? cameraInfos) => (super.noSuchMethod( Invocation.method( #filter, [cameraInfos], ), returnValue: - _i11.Future>.value(<_i2.CameraInfo>[]), + _i14.Future>.value(<_i2.CameraInfo>[]), returnValueForMissingStub: - _i11.Future>.value(<_i2.CameraInfo>[]), - ) as _i11.Future>); + _i14.Future>.value(<_i2.CameraInfo>[]), + ) as _i14.Future>); } /// A class which mocks [ExposureState]. @@ -331,55 +368,55 @@ class MockExposureState extends _i1.Mock implements _i3.ExposureState { /// A class which mocks [ImageAnalysis]. /// /// See the documentation for Mockito's code generation for more information. -class MockImageAnalysis extends _i1.Mock implements _i13.ImageAnalysis { +class MockImageAnalysis extends _i1.Mock implements _i16.ImageAnalysis { @override - _i11.Future setAnalyzer(_i14.Analyzer? analyzer) => (super.noSuchMethod( + _i14.Future setAnalyzer(_i17.Analyzer? analyzer) => (super.noSuchMethod( Invocation.method( #setAnalyzer, [analyzer], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i14.Future.value(), + returnValueForMissingStub: _i14.Future.value(), + ) as _i14.Future); @override - _i11.Future clearAnalyzer() => (super.noSuchMethod( + _i14.Future clearAnalyzer() => (super.noSuchMethod( Invocation.method( #clearAnalyzer, [], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i14.Future.value(), + returnValueForMissingStub: _i14.Future.value(), + ) as _i14.Future); } /// A class which mocks [ImageCapture]. /// /// See the documentation for Mockito's code generation for more information. -class MockImageCapture extends _i1.Mock implements _i15.ImageCapture { +class MockImageCapture extends _i1.Mock implements _i18.ImageCapture { @override - _i11.Future setFlashMode(int? newFlashMode) => (super.noSuchMethod( + _i14.Future setFlashMode(int? newFlashMode) => (super.noSuchMethod( Invocation.method( #setFlashMode, [newFlashMode], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i14.Future.value(), + returnValueForMissingStub: _i14.Future.value(), + ) as _i14.Future); @override - _i11.Future takePicture() => (super.noSuchMethod( + _i14.Future takePicture() => (super.noSuchMethod( Invocation.method( #takePicture, [], ), - returnValue: _i11.Future.value(''), - returnValueForMissingStub: _i11.Future.value(''), - ) as _i11.Future); + returnValue: _i14.Future.value(''), + returnValueForMissingStub: _i14.Future.value(''), + ) as _i14.Future); } /// A class which mocks [ImageProxy]. /// /// See the documentation for Mockito's code generation for more information. -class MockImageProxy extends _i1.Mock implements _i16.ImageProxy { +class MockImageProxy extends _i1.Mock implements _i19.ImageProxy { @override int get format => (super.noSuchMethod( Invocation.getter(#format), @@ -399,37 +436,37 @@ class MockImageProxy extends _i1.Mock implements _i16.ImageProxy { returnValueForMissingStub: 0, ) as int); @override - _i11.Future> getPlanes() => (super.noSuchMethod( + _i14.Future> getPlanes() => (super.noSuchMethod( Invocation.method( #getPlanes, [], ), returnValue: - _i11.Future>.value(<_i17.PlaneProxy>[]), + _i14.Future>.value(<_i20.PlaneProxy>[]), returnValueForMissingStub: - _i11.Future>.value(<_i17.PlaneProxy>[]), - ) as _i11.Future>); + _i14.Future>.value(<_i20.PlaneProxy>[]), + ) as _i14.Future>); @override - _i11.Future close() => (super.noSuchMethod( + _i14.Future close() => (super.noSuchMethod( Invocation.method( #close, [], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i14.Future.value(), + returnValueForMissingStub: _i14.Future.value(), + ) as _i14.Future); } /// A class which mocks [PlaneProxy]. /// /// See the documentation for Mockito's code generation for more information. -class MockPlaneProxy extends _i1.Mock implements _i17.PlaneProxy { +class MockPlaneProxy extends _i1.Mock implements _i20.PlaneProxy { @override - _i18.Uint8List get buffer => (super.noSuchMethod( + _i21.Uint8List get buffer => (super.noSuchMethod( Invocation.getter(#buffer), - returnValue: _i18.Uint8List(0), - returnValueForMissingStub: _i18.Uint8List(0), - ) as _i18.Uint8List); + returnValue: _i21.Uint8List(0), + returnValueForMissingStub: _i21.Uint8List(0), + ) as _i21.Uint8List); @override int get pixelStride => (super.noSuchMethod( Invocation.getter(#pixelStride), @@ -447,16 +484,16 @@ class MockPlaneProxy extends _i1.Mock implements _i17.PlaneProxy { /// A class which mocks [Preview]. /// /// See the documentation for Mockito's code generation for more information. -class MockPreview extends _i1.Mock implements _i19.Preview { +class MockPreview extends _i1.Mock implements _i22.Preview { @override - _i11.Future setSurfaceProvider() => (super.noSuchMethod( + _i14.Future setSurfaceProvider() => (super.noSuchMethod( Invocation.method( #setSurfaceProvider, [], ), - returnValue: _i11.Future.value(0), - returnValueForMissingStub: _i11.Future.value(0), - ) as _i11.Future); + returnValue: _i14.Future.value(0), + returnValueForMissingStub: _i14.Future.value(0), + ) as _i14.Future); @override void releaseFlutterSurfaceTexture() => super.noSuchMethod( Invocation.method( @@ -466,13 +503,13 @@ class MockPreview extends _i1.Mock implements _i19.Preview { returnValueForMissingStub: null, ); @override - _i11.Future<_i6.ResolutionInfo> getResolutionInfo() => (super.noSuchMethod( + _i14.Future<_i6.ResolutionInfo> getResolutionInfo() => (super.noSuchMethod( Invocation.method( #getResolutionInfo, [], ), returnValue: - _i11.Future<_i6.ResolutionInfo>.value(_FakeResolutionInfo_5( + _i14.Future<_i6.ResolutionInfo>.value(_FakeResolutionInfo_5( this, Invocation.method( #getResolutionInfo, @@ -480,37 +517,37 @@ class MockPreview extends _i1.Mock implements _i19.Preview { ), )), returnValueForMissingStub: - _i11.Future<_i6.ResolutionInfo>.value(_FakeResolutionInfo_5( + _i14.Future<_i6.ResolutionInfo>.value(_FakeResolutionInfo_5( this, Invocation.method( #getResolutionInfo, [], ), )), - ) as _i11.Future<_i6.ResolutionInfo>); + ) as _i14.Future<_i6.ResolutionInfo>); } /// A class which mocks [ProcessCameraProvider]. /// /// See the documentation for Mockito's code generation for more information. class MockProcessCameraProvider extends _i1.Mock - implements _i20.ProcessCameraProvider { + implements _i23.ProcessCameraProvider { @override - _i11.Future> getAvailableCameraInfos() => + _i14.Future> getAvailableCameraInfos() => (super.noSuchMethod( Invocation.method( #getAvailableCameraInfos, [], ), returnValue: - _i11.Future>.value(<_i2.CameraInfo>[]), + _i14.Future>.value(<_i2.CameraInfo>[]), returnValueForMissingStub: - _i11.Future>.value(<_i2.CameraInfo>[]), - ) as _i11.Future>); + _i14.Future>.value(<_i2.CameraInfo>[]), + ) as _i14.Future>); @override - _i11.Future<_i7.Camera> bindToLifecycle( - _i12.CameraSelector? cameraSelector, - List<_i21.UseCase>? useCases, + _i14.Future<_i7.Camera> bindToLifecycle( + _i15.CameraSelector? cameraSelector, + List<_i24.UseCase>? useCases, ) => (super.noSuchMethod( Invocation.method( @@ -520,7 +557,7 @@ class MockProcessCameraProvider extends _i1.Mock useCases, ], ), - returnValue: _i11.Future<_i7.Camera>.value(_FakeCamera_6( + returnValue: _i14.Future<_i7.Camera>.value(_FakeCamera_6( this, Invocation.method( #bindToLifecycle, @@ -530,7 +567,7 @@ class MockProcessCameraProvider extends _i1.Mock ], ), )), - returnValueForMissingStub: _i11.Future<_i7.Camera>.value(_FakeCamera_6( + returnValueForMissingStub: _i14.Future<_i7.Camera>.value(_FakeCamera_6( this, Invocation.method( #bindToLifecycle, @@ -540,18 +577,18 @@ class MockProcessCameraProvider extends _i1.Mock ], ), )), - ) as _i11.Future<_i7.Camera>); + ) as _i14.Future<_i7.Camera>); @override - _i11.Future isBound(_i21.UseCase? useCase) => (super.noSuchMethod( + _i14.Future isBound(_i24.UseCase? useCase) => (super.noSuchMethod( Invocation.method( #isBound, [useCase], ), - returnValue: _i11.Future.value(false), - returnValueForMissingStub: _i11.Future.value(false), - ) as _i11.Future); + returnValue: _i14.Future.value(false), + returnValueForMissingStub: _i14.Future.value(false), + ) as _i14.Future); @override - void unbind(List<_i21.UseCase>? useCases) => super.noSuchMethod( + void unbind(List<_i24.UseCase>? useCases) => super.noSuchMethod( Invocation.method( #unbind, [useCases], @@ -568,68 +605,165 @@ class MockProcessCameraProvider extends _i1.Mock ); } -/// A class which mocks [TestInstanceManagerHostApi]. +/// A class which mocks [Recorder]. /// /// See the documentation for Mockito's code generation for more information. -class MockTestInstanceManagerHostApi extends _i1.Mock - implements _i22.TestInstanceManagerHostApi { +class MockRecorder extends _i1.Mock implements _i10.Recorder { @override - void clear() => super.noSuchMethod( + _i14.Future<_i8.PendingRecording> prepareRecording(String? path) => + (super.noSuchMethod( Invocation.method( - #clear, + #prepareRecording, + [path], + ), + returnValue: + _i14.Future<_i8.PendingRecording>.value(_FakePendingRecording_7( + this, + Invocation.method( + #prepareRecording, + [path], + ), + )), + returnValueForMissingStub: + _i14.Future<_i8.PendingRecording>.value(_FakePendingRecording_7( + this, + Invocation.method( + #prepareRecording, + [path], + ), + )), + ) as _i14.Future<_i8.PendingRecording>); +} + +/// A class which mocks [PendingRecording]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPendingRecording extends _i1.Mock implements _i8.PendingRecording { + @override + _i14.Future<_i9.Recording> start() => (super.noSuchMethod( + Invocation.method( + #start, [], ), - returnValueForMissingStub: null, - ); + returnValue: _i14.Future<_i9.Recording>.value(_FakeRecording_8( + this, + Invocation.method( + #start, + [], + ), + )), + returnValueForMissingStub: + _i14.Future<_i9.Recording>.value(_FakeRecording_8( + this, + Invocation.method( + #start, + [], + ), + )), + ) as _i14.Future<_i9.Recording>); } -/// A class which mocks [ZoomState]. +/// A class which mocks [Recording]. /// /// See the documentation for Mockito's code generation for more information. -class MockZoomState extends _i1.Mock implements _i4.ZoomState { +class MockRecording extends _i1.Mock implements _i9.Recording { @override - double get minZoomRatio => (super.noSuchMethod( - Invocation.getter(#minZoomRatio), - returnValue: 0.0, - returnValueForMissingStub: 0.0, - ) as double); + _i14.Future close() => (super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValue: _i14.Future.value(), + returnValueForMissingStub: _i14.Future.value(), + ) as _i14.Future); @override - double get maxZoomRatio => (super.noSuchMethod( - Invocation.getter(#maxZoomRatio), - returnValue: 0.0, - returnValueForMissingStub: 0.0, - ) as double); + _i14.Future pause() => (super.noSuchMethod( + Invocation.method( + #pause, + [], + ), + returnValue: _i14.Future.value(), + returnValueForMissingStub: _i14.Future.value(), + ) as _i14.Future); + @override + _i14.Future resume() => (super.noSuchMethod( + Invocation.method( + #resume, + [], + ), + returnValue: _i14.Future.value(), + returnValueForMissingStub: _i14.Future.value(), + ) as _i14.Future); + @override + _i14.Future stop() => (super.noSuchMethod( + Invocation.method( + #stop, + [], + ), + returnValue: _i14.Future.value(), + returnValueForMissingStub: _i14.Future.value(), + ) as _i14.Future); } -/// A class which mocks [BuildContext]. +/// A class which mocks [VideoCapture]. /// /// See the documentation for Mockito's code generation for more information. -class MockBuildContext extends _i1.Mock implements _i8.BuildContext { - MockBuildContext() { - _i1.throwOnMissingStub(this); - } +class MockVideoCapture extends _i1.Mock implements _i25.VideoCapture { + @override + _i14.Future<_i10.Recorder> getOutput() => (super.noSuchMethod( + Invocation.method( + #getOutput, + [], + ), + returnValue: _i14.Future<_i10.Recorder>.value(_FakeRecorder_9( + this, + Invocation.method( + #getOutput, + [], + ), + )), + returnValueForMissingStub: + _i14.Future<_i10.Recorder>.value(_FakeRecorder_9( + this, + Invocation.method( + #getOutput, + [], + ), + )), + ) as _i14.Future<_i10.Recorder>); +} +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i11.BuildContext { @override - _i8.Widget get widget => (super.noSuchMethod( + _i11.Widget get widget => (super.noSuchMethod( Invocation.getter(#widget), - returnValue: _FakeWidget_7( + returnValue: _FakeWidget_10( this, Invocation.getter(#widget), ), - ) as _i8.Widget); + returnValueForMissingStub: _FakeWidget_10( + this, + Invocation.getter(#widget), + ), + ) as _i11.Widget); @override bool get mounted => (super.noSuchMethod( Invocation.getter(#mounted), returnValue: false, + returnValueForMissingStub: false, ) as bool); @override bool get debugDoingBuild => (super.noSuchMethod( Invocation.getter(#debugDoingBuild), returnValue: false, + returnValueForMissingStub: false, ) as bool); @override - _i8.InheritedWidget dependOnInheritedElement( - _i8.InheritedElement? ancestor, { + _i11.InheritedWidget dependOnInheritedElement( + _i11.InheritedElement? ancestor, { Object? aspect, }) => (super.noSuchMethod( @@ -638,7 +772,7 @@ class MockBuildContext extends _i1.Mock implements _i8.BuildContext { [ancestor], {#aspect: aspect}, ), - returnValue: _FakeInheritedWidget_8( + returnValue: _FakeInheritedWidget_11( this, Invocation.method( #dependOnInheritedElement, @@ -646,9 +780,17 @@ class MockBuildContext extends _i1.Mock implements _i8.BuildContext { {#aspect: aspect}, ), ), - ) as _i8.InheritedWidget); + returnValueForMissingStub: _FakeInheritedWidget_11( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) as _i11.InheritedWidget); @override - void visitAncestorElements(bool Function(_i8.Element)? visitor) => + void visitAncestorElements(bool Function(_i11.Element)? visitor) => super.noSuchMethod( Invocation.method( #visitAncestorElements, @@ -657,7 +799,7 @@ class MockBuildContext extends _i1.Mock implements _i8.BuildContext { returnValueForMissingStub: null, ); @override - void visitChildElements(_i8.ElementVisitor? visitor) => super.noSuchMethod( + void visitChildElements(_i11.ElementVisitor? visitor) => super.noSuchMethod( Invocation.method( #visitChildElements, [visitor], @@ -665,7 +807,7 @@ class MockBuildContext extends _i1.Mock implements _i8.BuildContext { returnValueForMissingStub: null, ); @override - void dispatchNotification(_i8.Notification? notification) => + void dispatchNotification(_i11.Notification? notification) => super.noSuchMethod( Invocation.method( #dispatchNotification, @@ -674,9 +816,9 @@ class MockBuildContext extends _i1.Mock implements _i8.BuildContext { returnValueForMissingStub: null, ); @override - _i10.DiagnosticsNode describeElement( + _i13.DiagnosticsNode describeElement( String? name, { - _i10.DiagnosticsTreeStyle? style = _i10.DiagnosticsTreeStyle.errorProperty, + _i13.DiagnosticsTreeStyle? style = _i13.DiagnosticsTreeStyle.errorProperty, }) => (super.noSuchMethod( Invocation.method( @@ -684,7 +826,15 @@ class MockBuildContext extends _i1.Mock implements _i8.BuildContext { [name], {#style: style}, ), - returnValue: _FakeDiagnosticsNode_9( + returnValue: _FakeDiagnosticsNode_12( + this, + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + ), + returnValueForMissingStub: _FakeDiagnosticsNode_12( this, Invocation.method( #describeElement, @@ -692,11 +842,11 @@ class MockBuildContext extends _i1.Mock implements _i8.BuildContext { {#style: style}, ), ), - ) as _i10.DiagnosticsNode); + ) as _i13.DiagnosticsNode); @override - _i10.DiagnosticsNode describeWidget( + _i13.DiagnosticsNode describeWidget( String? name, { - _i10.DiagnosticsTreeStyle? style = _i10.DiagnosticsTreeStyle.errorProperty, + _i13.DiagnosticsTreeStyle? style = _i13.DiagnosticsTreeStyle.errorProperty, }) => (super.noSuchMethod( Invocation.method( @@ -704,7 +854,15 @@ class MockBuildContext extends _i1.Mock implements _i8.BuildContext { [name], {#style: style}, ), - returnValue: _FakeDiagnosticsNode_9( + returnValue: _FakeDiagnosticsNode_12( + this, + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + ), + returnValueForMissingStub: _FakeDiagnosticsNode_12( this, Invocation.method( #describeWidget, @@ -712,9 +870,9 @@ class MockBuildContext extends _i1.Mock implements _i8.BuildContext { {#style: style}, ), ), - ) as _i10.DiagnosticsNode); + ) as _i13.DiagnosticsNode); @override - List<_i10.DiagnosticsNode> describeMissingAncestor( + List<_i13.DiagnosticsNode> describeMissingAncestor( {required Type? expectedAncestorType}) => (super.noSuchMethod( Invocation.method( @@ -722,21 +880,120 @@ class MockBuildContext extends _i1.Mock implements _i8.BuildContext { [], {#expectedAncestorType: expectedAncestorType}, ), - returnValue: <_i10.DiagnosticsNode>[], - ) as List<_i10.DiagnosticsNode>); + returnValue: <_i13.DiagnosticsNode>[], + returnValueForMissingStub: <_i13.DiagnosticsNode>[], + ) as List<_i13.DiagnosticsNode>); @override - _i10.DiagnosticsNode describeOwnershipChain(String? name) => + _i13.DiagnosticsNode describeOwnershipChain(String? name) => (super.noSuchMethod( Invocation.method( #describeOwnershipChain, [name], ), - returnValue: _FakeDiagnosticsNode_9( + returnValue: _FakeDiagnosticsNode_12( this, Invocation.method( #describeOwnershipChain, [name], ), ), - ) as _i10.DiagnosticsNode); + returnValueForMissingStub: _FakeDiagnosticsNode_12( + this, + Invocation.method( + #describeOwnershipChain, + [name], + ), + ), + ) as _i13.DiagnosticsNode); +} + +/// A class which mocks [TestInstanceManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestInstanceManagerHostApi extends _i1.Mock + implements _i26.TestInstanceManagerHostApi { + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [TestSystemServicesHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestSystemServicesHostApi extends _i1.Mock + implements _i26.TestSystemServicesHostApi { + @override + _i14.Future<_i6.CameraPermissionsErrorData?> requestCameraPermissions( + bool? enableAudio) => + (super.noSuchMethod( + Invocation.method( + #requestCameraPermissions, + [enableAudio], + ), + returnValue: _i14.Future<_i6.CameraPermissionsErrorData?>.value(), + returnValueForMissingStub: + _i14.Future<_i6.CameraPermissionsErrorData?>.value(), + ) as _i14.Future<_i6.CameraPermissionsErrorData?>); + @override + void startListeningForDeviceOrientationChange( + bool? isFrontFacing, + int? sensorOrientation, + ) => + super.noSuchMethod( + Invocation.method( + #startListeningForDeviceOrientationChange, + [ + isFrontFacing, + sensorOrientation, + ], + ), + returnValueForMissingStub: null, + ); + @override + void stopListeningForDeviceOrientationChange() => super.noSuchMethod( + Invocation.method( + #stopListeningForDeviceOrientationChange, + [], + ), + returnValueForMissingStub: null, + ); + @override + String getTempFilePath( + String? prefix, + String? suffix, + ) => + (super.noSuchMethod( + Invocation.method( + #getTempFilePath, + [ + prefix, + suffix, + ], + ), + returnValue: '', + returnValueForMissingStub: '', + ) as String); +} + +/// A class which mocks [ZoomState]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockZoomState extends _i1.Mock implements _i4.ZoomState { + @override + double get minZoomRatio => (super.noSuchMethod( + Invocation.getter(#minZoomRatio), + returnValue: 0.0, + returnValueForMissingStub: 0.0, + ) as double); + @override + double get maxZoomRatio => (super.noSuchMethod( + Invocation.getter(#maxZoomRatio), + returnValue: 0.0, + returnValueForMissingStub: 0.0, + ) as double); } diff --git a/packages/camera/camera_android_camerax/test/pending_recording_test.dart b/packages/camera/camera_android_camerax/test/pending_recording_test.dart new file mode 100644 index 00000000000..8957979cdd8 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/pending_recording_test.dart @@ -0,0 +1,65 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:camera_android_camerax/src/pending_recording.dart'; +import 'package:camera_android_camerax/src/recording.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'pending_recording_test.mocks.dart'; +import 'test_camerax_library.g.dart'; + +@GenerateMocks( + [TestPendingRecordingHostApi, TestInstanceManagerHostApi, Recording]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Mocks the call to clear the native InstanceManager. + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + test('start calls start on the Java side', () async { + final MockTestPendingRecordingHostApi mockApi = + MockTestPendingRecordingHostApi(); + TestPendingRecordingHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final PendingRecording pendingRecording = + PendingRecording.detached(instanceManager: instanceManager); + const int pendingRecordingId = 2; + instanceManager.addHostCreatedInstance(pendingRecording, pendingRecordingId, + onCopy: (_) => + PendingRecording.detached(instanceManager: instanceManager)); + + final Recording mockRecording = MockRecording(); + const int mockRecordingId = 3; + instanceManager.addHostCreatedInstance(mockRecording, mockRecordingId, + onCopy: (_) => Recording.detached(instanceManager: instanceManager)); + + when(mockApi.start(pendingRecordingId)).thenReturn(mockRecordingId); + expect(await pendingRecording.start(), mockRecording); + verify(mockApi.start(pendingRecordingId)); + }); + + test('flutterApiCreateTest', () async { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final PendingRecordingFlutterApi flutterApi = + PendingRecordingFlutterApiImpl( + instanceManager: instanceManager, + ); + + flutterApi.create(0); + + expect(instanceManager.getInstanceWithWeakReference(0), + isA()); + }); +} diff --git a/packages/camera/camera_android_camerax/test/pending_recording_test.mocks.dart b/packages/camera/camera_android_camerax/test/pending_recording_test.mocks.dart new file mode 100644 index 00000000000..95c3ad77115 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/pending_recording_test.mocks.dart @@ -0,0 +1,106 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in camera_android_camerax/test/pending_recording_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i4; + +import 'package:camera_android_camerax/src/recording.dart' as _i3; +import 'package:mockito/mockito.dart' as _i1; + +import 'test_camerax_library.g.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [TestPendingRecordingHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestPendingRecordingHostApi extends _i1.Mock + implements _i2.TestPendingRecordingHostApi { + MockTestPendingRecordingHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + int start(int? identifier) => (super.noSuchMethod( + Invocation.method( + #start, + [identifier], + ), + returnValue: 0, + ) as int); +} + +/// A class which mocks [TestInstanceManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestInstanceManagerHostApi extends _i1.Mock + implements _i2.TestInstanceManagerHostApi { + MockTestInstanceManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [Recording]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockRecording extends _i1.Mock implements _i3.Recording { + MockRecording() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.Future close() => (super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future pause() => (super.noSuchMethod( + Invocation.method( + #pause, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future resume() => (super.noSuchMethod( + Invocation.method( + #resume, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future stop() => (super.noSuchMethod( + Invocation.method( + #stop, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); +} diff --git a/packages/camera/camera_android_camerax/test/preview_test.dart b/packages/camera/camera_android_camerax/test/preview_test.dart index c0d287ec008..6eacd42b5ca 100644 --- a/packages/camera/camera_android_camerax/test/preview_test.dart +++ b/packages/camera/camera_android_camerax/test/preview_test.dart @@ -92,7 +92,7 @@ void main() { }); test( - 'releaseFlutterSurfaceTexture makes call to relase flutter surface texture entry', + 'releaseFlutterSurfaceTexture makes call to release flutter surface texture entry', () async { final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi(); TestPreviewHostApi.setup(mockApi); diff --git a/packages/camera/camera_android_camerax/test/recorder_test.dart b/packages/camera/camera_android_camerax/test/recorder_test.dart new file mode 100644 index 00000000000..6dc398bbdad --- /dev/null +++ b/packages/camera/camera_android_camerax/test/recorder_test.dart @@ -0,0 +1,113 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:camera_android_camerax/src/pending_recording.dart'; +import 'package:camera_android_camerax/src/recorder.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'recorder_test.mocks.dart'; +import 'test_camerax_library.g.dart'; + +@GenerateMocks( + [TestRecorderHostApi, TestInstanceManagerHostApi, PendingRecording]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Mocks the call to clear the native InstanceManager. + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + group('Recorder', () { + tearDown(() => TestCameraSelectorHostApi.setup(null)); + + test('detached create does not call create on the Java side', () async { + final MockTestRecorderHostApi mockApi = MockTestRecorderHostApi(); + TestRecorderHostApi.setup(mockApi); + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + Recorder.detached( + instanceManager: instanceManager, aspectRatio: 0, bitRate: 0); + + verifyNever(mockApi.create( + argThat(isA()), argThat(isA()), argThat(isA()))); + }); + + test('create does call create on the Java side', () async { + final MockTestRecorderHostApi mockApi = MockTestRecorderHostApi(); + TestRecorderHostApi.setup(mockApi); + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + const int aspectRatio = 1; + const int bitRate = 2; + + Recorder( + instanceManager: instanceManager, + aspectRatio: aspectRatio, + bitRate: bitRate); + + verify(mockApi.create(argThat(isA()), aspectRatio, bitRate)); + }); + + test('prepareRecording calls prepareRecording on Java side', () async { + final MockTestRecorderHostApi mockApi = MockTestRecorderHostApi(); + TestRecorderHostApi.setup(mockApi); + when(mockApi.prepareRecording(0, '/test/path')).thenAnswer((_) => 2); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + const String filePath = '/test/path'; + final Recorder recorder = + Recorder.detached(instanceManager: instanceManager); + const int recorderId = 0; + const int mockPendingRecordingId = 2; + + instanceManager.addHostCreatedInstance(recorder, recorderId, + onCopy: (_) => Recorder.detached(instanceManager: instanceManager)); + + final MockPendingRecording mockPendingRecording = MockPendingRecording(); + instanceManager.addHostCreatedInstance( + mockPendingRecording, mockPendingRecordingId, + onCopy: (_) => MockPendingRecording()); + when(mockApi.prepareRecording(recorderId, filePath)) + .thenReturn(mockPendingRecordingId); + final PendingRecording pendingRecording = + await recorder.prepareRecording(filePath); + expect(pendingRecording, mockPendingRecording); + }); + + test('flutterApiCreateTest', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final RecorderFlutterApiImpl flutterApi = RecorderFlutterApiImpl( + instanceManager: instanceManager, + ); + const int recorderId = 0; + const int aspectRatio = 1; + const int bitrate = 2; + + flutterApi.create(recorderId, aspectRatio, bitrate); + + expect(instanceManager.getInstanceWithWeakReference(recorderId), + isA()); + expect( + (instanceManager.getInstanceWithWeakReference(recorderId)! + as Recorder) + .aspectRatio, + equals(aspectRatio)); + expect( + (instanceManager.getInstanceWithWeakReference(0)! as Recorder) + .bitRate, + equals(bitrate)); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/recorder_test.mocks.dart b/packages/camera/camera_android_camerax/test/recorder_test.mocks.dart new file mode 100644 index 00000000000..10c709a057d --- /dev/null +++ b/packages/camera/camera_android_camerax/test/recorder_test.mocks.dart @@ -0,0 +1,135 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in camera_android_camerax/test/recorder_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; + +import 'package:camera_android_camerax/src/pending_recording.dart' as _i4; +import 'package:camera_android_camerax/src/recording.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +import 'test_camerax_library.g.dart' as _i3; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeRecording_0 extends _i1.SmartFake implements _i2.Recording { + _FakeRecording_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [TestRecorderHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestRecorderHostApi extends _i1.Mock + implements _i3.TestRecorderHostApi { + MockTestRecorderHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create( + int? identifier, + int? aspectRatio, + int? bitRate, + ) => + super.noSuchMethod( + Invocation.method( + #create, + [ + identifier, + aspectRatio, + bitRate, + ], + ), + returnValueForMissingStub: null, + ); + @override + int getAspectRatio(int? identifier) => (super.noSuchMethod( + Invocation.method( + #getAspectRatio, + [identifier], + ), + returnValue: 0, + ) as int); + @override + int getTargetVideoEncodingBitRate(int? identifier) => (super.noSuchMethod( + Invocation.method( + #getTargetVideoEncodingBitRate, + [identifier], + ), + returnValue: 0, + ) as int); + @override + int prepareRecording( + int? identifier, + String? path, + ) => + (super.noSuchMethod( + Invocation.method( + #prepareRecording, + [ + identifier, + path, + ], + ), + returnValue: 0, + ) as int); +} + +/// A class which mocks [TestInstanceManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestInstanceManagerHostApi extends _i1.Mock + implements _i3.TestInstanceManagerHostApi { + MockTestInstanceManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [PendingRecording]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPendingRecording extends _i1.Mock implements _i4.PendingRecording { + MockPendingRecording() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future<_i2.Recording> start() => (super.noSuchMethod( + Invocation.method( + #start, + [], + ), + returnValue: _i5.Future<_i2.Recording>.value(_FakeRecording_0( + this, + Invocation.method( + #start, + [], + ), + )), + ) as _i5.Future<_i2.Recording>); +} diff --git a/packages/camera/camera_android_camerax/test/recording_test.dart b/packages/camera/camera_android_camerax/test/recording_test.dart new file mode 100644 index 00000000000..06de01f42bc --- /dev/null +++ b/packages/camera/camera_android_camerax/test/recording_test.dart @@ -0,0 +1,119 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:camera_android_camerax/src/recording.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'recording_test.mocks.dart'; +import 'test_camerax_library.g.dart'; + +@GenerateMocks([TestRecordingHostApi, TestInstanceManagerHostApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Mocks the call to clear the native InstanceManager. + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + group('Recording', () { + tearDown(() => TestRecorderHostApi.setup(null)); + + test('close calls close on Java side', () async { + final MockTestRecordingHostApi mockApi = MockTestRecordingHostApi(); + TestRecordingHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final Recording recording = + Recording.detached(instanceManager: instanceManager); + const int recordingId = 0; + when(mockApi.close(recordingId)).thenAnswer((_) {}); + instanceManager.addHostCreatedInstance(recording, recordingId, + onCopy: (_) => Recording.detached(instanceManager: instanceManager)); + + recording.close(); + + verify(mockApi.close(recordingId)); + }); + + test('pause calls pause on Java side', () async { + final MockTestRecordingHostApi mockApi = MockTestRecordingHostApi(); + TestRecordingHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final Recording recording = + Recording.detached(instanceManager: instanceManager); + const int recordingId = 0; + when(mockApi.pause(recordingId)).thenAnswer((_) {}); + instanceManager.addHostCreatedInstance(recording, recordingId, + onCopy: (_) => Recording.detached(instanceManager: instanceManager)); + + recording.pause(); + + verify(mockApi.pause(recordingId)); + }); + + test('resume calls resume on Java side', () async { + final MockTestRecordingHostApi mockApi = MockTestRecordingHostApi(); + TestRecordingHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final Recording recording = + Recording.detached(instanceManager: instanceManager); + const int recordingId = 0; + when(mockApi.resume(recordingId)).thenAnswer((_) {}); + instanceManager.addHostCreatedInstance(recording, recordingId, + onCopy: (_) => Recording.detached(instanceManager: instanceManager)); + + recording.resume(); + + verify(mockApi.resume(recordingId)); + }); + + test('stop calls stop on Java side', () async { + final MockTestRecordingHostApi mockApi = MockTestRecordingHostApi(); + TestRecordingHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final Recording recording = + Recording.detached(instanceManager: instanceManager); + const int recordingId = 0; + when(mockApi.stop(recordingId)).thenAnswer((_) {}); + instanceManager.addHostCreatedInstance(recording, recordingId, + onCopy: (_) => Recording.detached(instanceManager: instanceManager)); + + recording.stop(); + + verify(mockApi.stop(recordingId)); + }); + + test('flutterApiCreateTest', () async { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final RecordingFlutterApi flutterApi = RecordingFlutterApiImpl( + instanceManager: instanceManager, + ); + + flutterApi.create(0); + + expect(instanceManager.getInstanceWithWeakReference(0), isA()); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/recording_test.mocks.dart b/packages/camera/camera_android_camerax/test/recording_test.mocks.dart new file mode 100644 index 00000000000..0334ae2833d --- /dev/null +++ b/packages/camera/camera_android_camerax/test/recording_test.mocks.dart @@ -0,0 +1,81 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in camera_android_camerax/test/recording_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:mockito/mockito.dart' as _i1; + +import 'test_camerax_library.g.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [TestRecordingHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestRecordingHostApi extends _i1.Mock + implements _i2.TestRecordingHostApi { + MockTestRecordingHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void close(int? identifier) => super.noSuchMethod( + Invocation.method( + #close, + [identifier], + ), + returnValueForMissingStub: null, + ); + @override + void pause(int? identifier) => super.noSuchMethod( + Invocation.method( + #pause, + [identifier], + ), + returnValueForMissingStub: null, + ); + @override + void resume(int? identifier) => super.noSuchMethod( + Invocation.method( + #resume, + [identifier], + ), + returnValueForMissingStub: null, + ); + @override + void stop(int? identifier) => super.noSuchMethod( + Invocation.method( + #stop, + [identifier], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [TestInstanceManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestInstanceManagerHostApi extends _i1.Mock + implements _i2.TestInstanceManagerHostApi { + MockTestInstanceManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); +} diff --git a/packages/camera/camera_android_camerax/test/system_services_test.dart b/packages/camera/camera_android_camerax/test/system_services_test.dart index 0f0ecfd12ce..414097b6015 100644 --- a/packages/camera/camera_android_camerax/test/system_services_test.dart +++ b/packages/camera/camera_android_camerax/test/system_services_test.dart @@ -109,5 +109,20 @@ void main() { }); SystemServicesFlutterApiImpl().onCameraError(testErrorDescription); }); + + test('getTempFilePath completes normally', () async { + final MockTestSystemServicesHostApi mockApi = + MockTestSystemServicesHostApi(); + TestSystemServicesHostApi.setup(mockApi); + const String testPath = '/test/path/'; + const String testPrefix = 'MOV'; + const String testSuffix = '.mp4'; + + when(mockApi.getTempFilePath(testPrefix, testSuffix)) + .thenReturn(testPath + testPrefix + testSuffix); + expect(await SystemServices.getTempFilePath(testPrefix, testSuffix), + testPath + testPrefix + testSuffix); + verify(mockApi.getTempFilePath(testPrefix, testSuffix)); + }); }); } diff --git a/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart b/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart index cb0acdaba0b..f268a350bb3 100644 --- a/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart @@ -82,4 +82,19 @@ class MockTestSystemServicesHostApi extends _i1.Mock ), returnValueForMissingStub: null, ); + @override + String getTempFilePath( + String? prefix, + String? suffix, + ) => + (super.noSuchMethod( + Invocation.method( + #getTempFilePath, + [ + prefix, + suffix, + ], + ), + returnValue: '', + ) as String); } diff --git a/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart index 40bd8f2e433..8f83b076793 100644 --- a/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart +++ b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import // ignore_for_file: avoid_relative_lib_imports @@ -465,6 +465,8 @@ abstract class TestSystemServicesHostApi { void stopListeningForDeviceOrientationChange(); + String getTempFilePath(String prefix, String suffix); + static void setup(TestSystemServicesHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -536,6 +538,31 @@ abstract class TestSystemServicesHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath was null.'); + final List args = (message as List?)!; + final String? arg_prefix = (args[0] as String?); + assert(arg_prefix != null, + 'Argument for dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath was null, expected non-null String.'); + final String? arg_suffix = (args[1] as String?); + assert(arg_suffix != null, + 'Argument for dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath was null, expected non-null String.'); + final String output = api.getTempFilePath(arg_prefix!, arg_suffix!); + return [output]; + }); + } + } } } @@ -672,6 +699,316 @@ abstract class TestPreviewHostApi { } } +abstract class TestVideoCaptureHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + int withOutput(int videoOutputId); + + int getOutput(int identifier); + + static void setup(TestVideoCaptureHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoCaptureHostApi.withOutput', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoCaptureHostApi.withOutput was null.'); + final List args = (message as List?)!; + final int? arg_videoOutputId = (args[0] as int?); + assert(arg_videoOutputId != null, + 'Argument for dev.flutter.pigeon.VideoCaptureHostApi.withOutput was null, expected non-null int.'); + final int output = api.withOutput(arg_videoOutputId!); + return [output]; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoCaptureHostApi.getOutput', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoCaptureHostApi.getOutput was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.VideoCaptureHostApi.getOutput was null, expected non-null int.'); + final int output = api.getOutput(arg_identifier!); + return [output]; + }); + } + } + } +} + +abstract class TestRecorderHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + void create(int identifier, int? aspectRatio, int? bitRate); + + int getAspectRatio(int identifier); + + int getTargetVideoEncodingBitRate(int identifier); + + int prepareRecording(int identifier, String path); + + static void setup(TestRecorderHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecorderHostApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.RecorderHostApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.RecorderHostApi.create was null, expected non-null int.'); + final int? arg_aspectRatio = (args[1] as int?); + final int? arg_bitRate = (args[2] as int?); + api.create(arg_identifier!, arg_aspectRatio, arg_bitRate); + return []; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecorderHostApi.getAspectRatio', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.RecorderHostApi.getAspectRatio was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.RecorderHostApi.getAspectRatio was null, expected non-null int.'); + final int output = api.getAspectRatio(arg_identifier!); + return [output]; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecorderHostApi.getTargetVideoEncodingBitRate', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.RecorderHostApi.getTargetVideoEncodingBitRate was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.RecorderHostApi.getTargetVideoEncodingBitRate was null, expected non-null int.'); + final int output = api.getTargetVideoEncodingBitRate(arg_identifier!); + return [output]; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecorderHostApi.prepareRecording', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.RecorderHostApi.prepareRecording was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.RecorderHostApi.prepareRecording was null, expected non-null int.'); + final String? arg_path = (args[1] as String?); + assert(arg_path != null, + 'Argument for dev.flutter.pigeon.RecorderHostApi.prepareRecording was null, expected non-null String.'); + final int output = api.prepareRecording(arg_identifier!, arg_path!); + return [output]; + }); + } + } + } +} + +abstract class TestPendingRecordingHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + int start(int identifier); + + static void setup(TestPendingRecordingHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PendingRecordingHostApi.start', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PendingRecordingHostApi.start was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.PendingRecordingHostApi.start was null, expected non-null int.'); + final int output = api.start(arg_identifier!); + return [output]; + }); + } + } + } +} + +abstract class TestRecordingHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + void close(int identifier); + + void pause(int identifier); + + void resume(int identifier); + + void stop(int identifier); + + static void setup(TestRecordingHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecordingHostApi.close', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.RecordingHostApi.close was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.RecordingHostApi.close was null, expected non-null int.'); + api.close(arg_identifier!); + return []; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecordingHostApi.pause', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.RecordingHostApi.pause was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.RecordingHostApi.pause was null, expected non-null int.'); + api.pause(arg_identifier!); + return []; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecordingHostApi.resume', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.RecordingHostApi.resume was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.RecordingHostApi.resume was null, expected non-null int.'); + api.resume(arg_identifier!); + return []; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.RecordingHostApi.stop', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.RecordingHostApi.stop was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.RecordingHostApi.stop was null, expected non-null int.'); + api.stop(arg_identifier!); + return []; + }); + } + } + } +} + class _TestImageCaptureHostApiCodec extends StandardMessageCodec { const _TestImageCaptureHostApiCodec(); @override diff --git a/packages/camera/camera_android_camerax/test/video_capture_test.dart b/packages/camera/camera_android_camerax/test/video_capture_test.dart new file mode 100644 index 00000000000..560c48ffd59 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/video_capture_test.dart @@ -0,0 +1,90 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:camera_android_camerax/src/recorder.dart'; +import 'package:camera_android_camerax/src/video_capture.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'test_camerax_library.g.dart'; +import 'video_capture_test.mocks.dart'; + +@GenerateMocks( + [TestVideoCaptureHostApi, TestInstanceManagerHostApi, Recorder]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Mocks the call to clear the native InstanceManager. + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + test('withOutput calls the Java side and returns correct video capture', + () async { + final MockTestVideoCaptureHostApi mockApi = MockTestVideoCaptureHostApi(); + TestVideoCaptureHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final Recorder mockRecorder = MockRecorder(); + const int mockRecorderId = 2; + instanceManager.addHostCreatedInstance(mockRecorder, mockRecorderId, + onCopy: (_) => MockRecorder()); + + final VideoCapture videoCapture = + VideoCapture.detached(instanceManager: instanceManager); + const int videoCaptureId = 3; + instanceManager.addHostCreatedInstance(videoCapture, videoCaptureId, + onCopy: (_) => VideoCapture.detached(instanceManager: instanceManager)); + + when(mockApi.withOutput(mockRecorderId)).thenReturn(videoCaptureId); + + expect( + await VideoCapture.withOutput(mockRecorder, + instanceManager: instanceManager), + videoCapture); + verify(mockApi.withOutput(mockRecorderId)); + }); + + test('getOutput calls the Java side and returns correct Recorder', () async { + final MockTestVideoCaptureHostApi mockApi = MockTestVideoCaptureHostApi(); + TestVideoCaptureHostApi.setup(mockApi); + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final VideoCapture videoCapture = + VideoCapture.detached(instanceManager: instanceManager); + const int videoCaptureId = 2; + instanceManager.addHostCreatedInstance(videoCapture, videoCaptureId, + onCopy: (_) => VideoCapture.detached(instanceManager: instanceManager)); + + final Recorder mockRecorder = MockRecorder(); + const int mockRecorderId = 3; + instanceManager.addHostCreatedInstance(mockRecorder, mockRecorderId, + onCopy: (_) => Recorder.detached(instanceManager: instanceManager)); + + when(mockApi.getOutput(videoCaptureId)).thenReturn(mockRecorderId); + expect(await videoCapture.getOutput(), mockRecorder); + verify(mockApi.getOutput(videoCaptureId)); + }); + + test('flutterApiCreateTest', () async { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final VideoCaptureFlutterApi flutterApi = VideoCaptureFlutterApiImpl( + instanceManager: instanceManager, + ); + + flutterApi.create(0); + + expect( + instanceManager.getInstanceWithWeakReference(0), isA()); + }); +} diff --git a/packages/camera/camera_android_camerax/test/video_capture_test.mocks.dart b/packages/camera/camera_android_camerax/test/video_capture_test.mocks.dart new file mode 100644 index 00000000000..a8740b3d1a4 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/video_capture_test.mocks.dart @@ -0,0 +1,106 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in camera_android_camerax/test/video_capture_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; + +import 'package:camera_android_camerax/src/pending_recording.dart' as _i2; +import 'package:camera_android_camerax/src/recorder.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; + +import 'test_camerax_library.g.dart' as _i3; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakePendingRecording_0 extends _i1.SmartFake + implements _i2.PendingRecording { + _FakePendingRecording_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [TestVideoCaptureHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestVideoCaptureHostApi extends _i1.Mock + implements _i3.TestVideoCaptureHostApi { + MockTestVideoCaptureHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + int withOutput(int? videoOutputId) => (super.noSuchMethod( + Invocation.method( + #withOutput, + [videoOutputId], + ), + returnValue: 0, + ) as int); + @override + int getOutput(int? identifier) => (super.noSuchMethod( + Invocation.method( + #getOutput, + [identifier], + ), + returnValue: 0, + ) as int); +} + +/// A class which mocks [TestInstanceManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestInstanceManagerHostApi extends _i1.Mock + implements _i3.TestInstanceManagerHostApi { + MockTestInstanceManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [Recorder]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockRecorder extends _i1.Mock implements _i4.Recorder { + MockRecorder() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future<_i2.PendingRecording> prepareRecording(String? path) => + (super.noSuchMethod( + Invocation.method( + #prepareRecording, + [path], + ), + returnValue: + _i5.Future<_i2.PendingRecording>.value(_FakePendingRecording_0( + this, + Invocation.method( + #prepareRecording, + [path], + ), + )), + ) as _i5.Future<_i2.PendingRecording>); +}