diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 10531bd360b9..d3019e33eaa9 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.7.6+1 + +* Fixes video recording duration discrepancy by waiting for the first recorded frame before completing `startVideoCapturing`, and validates errors on `stopVideoRecording`. + ## 0.7.6 * Adds support for zero-shutter-lag still image capture via diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt index c871645e206f..5dc7fb05ab8b 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt @@ -425,13 +425,17 @@ abstract class CameraXLibraryPigeonProxyApiRegistrar(val binaryMessenger: Binary */ abstract fun getPigeonApiCameraIntegerRange(): PigeonApiCameraIntegerRange + /** + * An implementation of [PigeonApiRecordingStats] used to add a new Dart instance of + * `RecordingStats` to the Dart `InstanceManager`. + */ + abstract fun getPigeonApiRecordingStats(): PigeonApiRecordingStats + /** * An implementation of [PigeonApiVideoRecordEvent] used to add a new Dart instance of * `VideoRecordEvent` to the Dart `InstanceManager`. */ - open fun getPigeonApiVideoRecordEvent(): PigeonApiVideoRecordEvent { - return PigeonApiVideoRecordEvent(this) - } + abstract fun getPigeonApiVideoRecordEvent(): PigeonApiVideoRecordEvent /** * An implementation of [PigeonApiVideoRecordEventStart] used to add a new Dart instance of @@ -441,13 +445,19 @@ abstract class CameraXLibraryPigeonProxyApiRegistrar(val binaryMessenger: Binary return PigeonApiVideoRecordEventStart(this) } + /** + * An implementation of [PigeonApiVideoRecordEventStatus] used to add a new Dart instance of + * `VideoRecordEventStatus` to the Dart `InstanceManager`. + */ + open fun getPigeonApiVideoRecordEventStatus(): PigeonApiVideoRecordEventStatus { + return PigeonApiVideoRecordEventStatus(this) + } + /** * An implementation of [PigeonApiVideoRecordEventFinalize] used to add a new Dart instance of * `VideoRecordEventFinalize` to the Dart `InstanceManager`. */ - open fun getPigeonApiVideoRecordEventFinalize(): PigeonApiVideoRecordEventFinalize { - return PigeonApiVideoRecordEventFinalize(this) - } + abstract fun getPigeonApiVideoRecordEventFinalize(): PigeonApiVideoRecordEventFinalize /** * An implementation of [PigeonApiMeteringPoint] used to add a new Dart instance of @@ -745,6 +755,7 @@ abstract class CameraXLibraryPigeonProxyApiRegistrar(val binaryMessenger: Binary PigeonApiCameraSize.setUpMessageHandlers(binaryMessenger, getPigeonApiCameraSize()) PigeonApiCameraIntegerRange.setUpMessageHandlers( binaryMessenger, getPigeonApiCameraIntegerRange()) + PigeonApiVideoRecordEvent.setUpMessageHandlers(binaryMessenger, getPigeonApiVideoRecordEvent()) PigeonApiMeteringPoint.setUpMessageHandlers(binaryMessenger, getPigeonApiMeteringPoint()) PigeonApiObserver.setUpMessageHandlers(binaryMessenger, getPigeonApiObserver()) PigeonApiCameraInfo.setUpMessageHandlers(binaryMessenger, getPigeonApiCameraInfo()) @@ -800,6 +811,7 @@ abstract class CameraXLibraryPigeonProxyApiRegistrar(val binaryMessenger: Binary CameraXLibraryPigeonInstanceManagerApi.setUpMessageHandlers(binaryMessenger, null) PigeonApiCameraSize.setUpMessageHandlers(binaryMessenger, null) PigeonApiCameraIntegerRange.setUpMessageHandlers(binaryMessenger, null) + PigeonApiVideoRecordEvent.setUpMessageHandlers(binaryMessenger, null) PigeonApiMeteringPoint.setUpMessageHandlers(binaryMessenger, null) PigeonApiObserver.setUpMessageHandlers(binaryMessenger, null) PigeonApiCameraInfo.setUpMessageHandlers(binaryMessenger, null) @@ -908,12 +920,24 @@ private class CameraXLibraryPigeonProxyApiBaseCodec( logNewInstanceFailure("CameraIntegerRange", value, it.exceptionOrNull()) } } + } else if (value is androidx.camera.video.RecordingStats) { + registrar.getPigeonApiRecordingStats().pigeon_newInstance(value) { + if (it.isFailure) { + logNewInstanceFailure("RecordingStats", value, it.exceptionOrNull()) + } + } } else if (value is androidx.camera.video.VideoRecordEvent.Start) { registrar.getPigeonApiVideoRecordEventStart().pigeon_newInstance(value) { if (it.isFailure) { logNewInstanceFailure("VideoRecordEventStart", value, it.exceptionOrNull()) } } + } else if (value is androidx.camera.video.VideoRecordEvent.Status) { + registrar.getPigeonApiVideoRecordEventStatus().pigeon_newInstance(value) { + if (it.isFailure) { + logNewInstanceFailure("VideoRecordEventStatus", value, it.exceptionOrNull()) + } + } } else if (value is androidx.camera.video.VideoRecordEvent.Finalize) { registrar.getPigeonApiVideoRecordEventFinalize().pigeon_newInstance(value) { if (it.isFailure) { @@ -1840,15 +1864,101 @@ abstract class PigeonApiCameraIntegerRange( } } } +/** + * RecordingStats provides recording statistics such as duration and bytes recorded. + * + * See https://developer.android.com/reference/androidx/camera/video/RecordingStats. + */ +@Suppress("UNCHECKED_CAST") +abstract class PigeonApiRecordingStats( + open val pigeonRegistrar: CameraXLibraryPigeonProxyApiRegistrar +) { + /** The recorded duration in nanoseconds. */ + abstract fun recordedDurationNanos(pigeon_instance: androidx.camera.video.RecordingStats): Long + + /** The number of bytes recorded. */ + abstract fun numBytesRecorded(pigeon_instance: androidx.camera.video.RecordingStats): Long + + @Suppress("LocalVariableName", "FunctionName") + /** Creates a Dart instance of RecordingStats and attaches it to [pigeon_instanceArg]. */ + fun pigeon_newInstance( + pigeon_instanceArg: androidx.camera.video.RecordingStats, + callback: (Result) -> Unit + ) { + if (pigeonRegistrar.ignoreCallsToDart) { + callback( + Result.failure( + CameraXError("ignore-calls-error", "Calls to Dart are being ignored.", ""))) + } else if (pigeonRegistrar.instanceManager.containsInstance(pigeon_instanceArg)) { + callback(Result.success(Unit)) + } else { + val pigeon_identifierArg = + pigeonRegistrar.instanceManager.addHostCreatedInstance(pigeon_instanceArg) + val recordedDurationNanosArg = recordedDurationNanos(pigeon_instanceArg) + val numBytesRecordedArg = numBytesRecorded(pigeon_instanceArg) + val binaryMessenger = pigeonRegistrar.binaryMessenger + val codec = pigeonRegistrar.codec + val channelName = + "dev.flutter.pigeon.camera_android_camerax.RecordingStats.pigeon_newInstance" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(pigeon_identifierArg, recordedDurationNanosArg, numBytesRecordedArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback( + Result.failure(CameraXError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(CameraXLibraryPigeonUtils.createConnectionError(channelName))) + } + } + } + } +} /** * VideoRecordEvent is used to report video recording events and status. * * See https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent. */ @Suppress("UNCHECKED_CAST") -open class PigeonApiVideoRecordEvent( +abstract class PigeonApiVideoRecordEvent( open val pigeonRegistrar: CameraXLibraryPigeonProxyApiRegistrar ) { + /** The recording statistics for this event. */ + abstract fun getRecordingStats( + pigeon_instance: androidx.camera.video.VideoRecordEvent + ): androidx.camera.video.RecordingStats + + companion object { + @Suppress("LocalVariableName") + fun setUpMessageHandlers(binaryMessenger: BinaryMessenger, api: PigeonApiVideoRecordEvent?) { + val codec = api?.pigeonRegistrar?.codec ?: CameraXLibraryPigeonCodec() + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.camera_android_camerax.VideoRecordEvent.getRecordingStats", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val pigeon_instanceArg = args[0] as androidx.camera.video.VideoRecordEvent + val wrapped: List = + try { + listOf(api.getRecordingStats(pigeon_instanceArg)) + } catch (exception: Throwable) { + CameraXLibraryPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } + @Suppress("LocalVariableName", "FunctionName") /** Creates a Dart instance of VideoRecordEvent and attaches it to [pigeon_instanceArg]. */ fun pigeon_newInstance( @@ -1934,15 +2044,81 @@ open class PigeonApiVideoRecordEventStart( return pigeonRegistrar.getPigeonApiVideoRecordEvent() } } +/** + * Indicates recording status updates. + * + * See https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent.Status. + */ +@Suppress("UNCHECKED_CAST") +open class PigeonApiVideoRecordEventStatus( + open val pigeonRegistrar: CameraXLibraryPigeonProxyApiRegistrar +) { + @Suppress("LocalVariableName", "FunctionName") + /** Creates a Dart instance of VideoRecordEventStatus and attaches it to [pigeon_instanceArg]. */ + fun pigeon_newInstance( + pigeon_instanceArg: androidx.camera.video.VideoRecordEvent.Status, + callback: (Result) -> Unit + ) { + if (pigeonRegistrar.ignoreCallsToDart) { + callback( + Result.failure( + CameraXError("ignore-calls-error", "Calls to Dart are being ignored.", ""))) + } else if (pigeonRegistrar.instanceManager.containsInstance(pigeon_instanceArg)) { + callback(Result.success(Unit)) + } else { + val pigeon_identifierArg = + pigeonRegistrar.instanceManager.addHostCreatedInstance(pigeon_instanceArg) + val binaryMessenger = pigeonRegistrar.binaryMessenger + val codec = pigeonRegistrar.codec + val channelName = + "dev.flutter.pigeon.camera_android_camerax.VideoRecordEventStatus.pigeon_newInstance" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(pigeon_identifierArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback( + Result.failure(CameraXError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(CameraXLibraryPigeonUtils.createConnectionError(channelName))) + } + } + } + } + + @Suppress("FunctionName") + /** An implementation of [PigeonApiVideoRecordEvent] used to access callback methods */ + fun pigeon_getPigeonApiVideoRecordEvent(): PigeonApiVideoRecordEvent { + return pigeonRegistrar.getPigeonApiVideoRecordEvent() + } +} /** * Indicates the finalization of recording. * * See https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent.Finalize. */ @Suppress("UNCHECKED_CAST") -open class PigeonApiVideoRecordEventFinalize( +abstract class PigeonApiVideoRecordEventFinalize( open val pigeonRegistrar: CameraXLibraryPigeonProxyApiRegistrar ) { + /** + * The error code of the finalization. + * + * See + * https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent.Finalize#getError(). + */ + abstract fun error(pigeon_instance: androidx.camera.video.VideoRecordEvent.Finalize): Long + + /** + * Whether the recording finalized with an error. + * + * See + * https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent.Finalize#hasError(). + */ + abstract fun hasError(pigeon_instance: androidx.camera.video.VideoRecordEvent.Finalize): Boolean + @Suppress("LocalVariableName", "FunctionName") /** * Creates a Dart instance of VideoRecordEventFinalize and attaches it to [pigeon_instanceArg]. @@ -1960,12 +2136,14 @@ open class PigeonApiVideoRecordEventFinalize( } else { val pigeon_identifierArg = pigeonRegistrar.instanceManager.addHostCreatedInstance(pigeon_instanceArg) + val errorArg = error(pigeon_instanceArg) + val hasErrorArg = hasError(pigeon_instanceArg) val binaryMessenger = pigeonRegistrar.binaryMessenger val codec = pigeonRegistrar.codec val channelName = "dev.flutter.pigeon.camera_android_camerax.VideoRecordEventFinalize.pigeon_newInstance" val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(pigeon_identifierArg)) { + channel.send(listOf(pigeon_identifierArg, errorArg, hasErrorArg)) { if (it is List<*>) { if (it.size > 1) { callback( diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProxyApiRegistrar.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProxyApiRegistrar.java index 3941932385c8..67d01be8d856 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProxyApiRegistrar.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProxyApiRegistrar.java @@ -250,6 +250,24 @@ public PigeonApiVideoRecordEventListener getPigeonApiVideoRecordEventListener() return new VideoRecordEventListenerProxyApi(this); } + @NonNull + @Override + public PigeonApiRecordingStats getPigeonApiRecordingStats() { + return new RecordingStatsProxyApi(this); + } + + @NonNull + @Override + public PigeonApiVideoRecordEvent getPigeonApiVideoRecordEvent() { + return new VideoRecordEventProxyApi(this); + } + + @NonNull + @Override + public PigeonApiVideoRecordEventFinalize getPigeonApiVideoRecordEventFinalize() { + return new VideoRecordEventFinalizeProxyApi(this); + } + @NonNull @Override public PigeonApiPendingRecording getPigeonApiPendingRecording() { diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecordingStatsProxyApi.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecordingStatsProxyApi.java new file mode 100644 index 000000000000..113f772c5fd4 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/RecordingStatsProxyApi.java @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors +// 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.RecordingStats; + +/** ProxyApi implementation for {@link RecordingStats}. */ +class RecordingStatsProxyApi extends PigeonApiRecordingStats { + RecordingStatsProxyApi(@NonNull ProxyApiRegistrar pigeonRegistrar) { + super(pigeonRegistrar); + } + + @Override + public long recordedDurationNanos(@NonNull RecordingStats pigeonInstance) { + return pigeonInstance.getRecordedDurationNanos(); + } + + @Override + public long numBytesRecorded(@NonNull RecordingStats pigeonInstance) { + return pigeonInstance.getNumBytesRecorded(); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoRecordEventFinalizeProxyApi.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoRecordEventFinalizeProxyApi.java new file mode 100644 index 000000000000..637122717935 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoRecordEventFinalizeProxyApi.java @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors +// 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.VideoRecordEvent; + +/** ProxyApi implementation for {@link VideoRecordEvent.Finalize}. */ +class VideoRecordEventFinalizeProxyApi extends PigeonApiVideoRecordEventFinalize { + VideoRecordEventFinalizeProxyApi(@NonNull ProxyApiRegistrar pigeonRegistrar) { + super(pigeonRegistrar); + } + + @Override + public long error(@NonNull VideoRecordEvent.Finalize pigeonInstance) { + return (long) pigeonInstance.getError(); + } + + @Override + public boolean hasError(@NonNull VideoRecordEvent.Finalize pigeonInstance) { + return pigeonInstance.hasError(); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoRecordEventProxyApi.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoRecordEventProxyApi.java new file mode 100644 index 000000000000..ebcd91beaa72 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoRecordEventProxyApi.java @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors +// 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.RecordingStats; +import androidx.camera.video.VideoRecordEvent; + +/** ProxyApi implementation for {@link VideoRecordEvent}. */ +class VideoRecordEventProxyApi extends PigeonApiVideoRecordEvent { + VideoRecordEventProxyApi(@NonNull ProxyApiRegistrar pigeonRegistrar) { + super(pigeonRegistrar); + } + + @NonNull + @Override + public RecordingStats getRecordingStats(@NonNull VideoRecordEvent pigeonInstance) { + return pigeonInstance.getRecordingStats(); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/RecordingStatsTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/RecordingStatsTest.java new file mode 100644 index 000000000000..085ffb20ab77 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/RecordingStatsTest.java @@ -0,0 +1,34 @@ +// Copyright 2013 The Flutter Authors +// 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.Mockito.mock; +import static org.mockito.Mockito.when; + +import androidx.camera.video.RecordingStats; +import org.junit.Test; + +public class RecordingStatsTest { + @Test + public void recordedDurationNanos_returnsExpectedValue() { + final PigeonApiRecordingStats api = new TestProxyApiRegistrar().getPigeonApiRecordingStats(); + final RecordingStats instance = mock(RecordingStats.class); + + when(instance.getRecordedDurationNanos()).thenReturn(123456789L); + + assertEquals(123456789L, api.recordedDurationNanos(instance)); + } + + @Test + public void numBytesRecorded_returnsExpectedValue() { + final PigeonApiRecordingStats api = new TestProxyApiRegistrar().getPigeonApiRecordingStats(); + final RecordingStats instance = mock(RecordingStats.class); + + when(instance.getNumBytesRecorded()).thenReturn(987654L); + + assertEquals(987654L, api.numBytesRecorded(instance)); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/VideoRecordEventFinalizeTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/VideoRecordEventFinalizeTest.java new file mode 100644 index 000000000000..eb3ff446fe1a --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/VideoRecordEventFinalizeTest.java @@ -0,0 +1,36 @@ +// Copyright 2013 The Flutter Authors +// 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.Mockito.mock; +import static org.mockito.Mockito.when; + +import androidx.camera.video.VideoRecordEvent; +import org.junit.Test; + +public class VideoRecordEventFinalizeTest { + @Test + public void error_returnsExpectedValue() { + final PigeonApiVideoRecordEventFinalize api = + new TestProxyApiRegistrar().getPigeonApiVideoRecordEventFinalize(); + final VideoRecordEvent.Finalize instance = mock(VideoRecordEvent.Finalize.class); + + when(instance.getError()).thenReturn(VideoRecordEvent.Finalize.ERROR_NO_VALID_DATA); + + assertEquals((long) VideoRecordEvent.Finalize.ERROR_NO_VALID_DATA, api.error(instance)); + } + + @Test + public void hasError_returnsExpectedValue() { + final PigeonApiVideoRecordEventFinalize api = + new TestProxyApiRegistrar().getPigeonApiVideoRecordEventFinalize(); + final VideoRecordEvent.Finalize instance = mock(VideoRecordEvent.Finalize.class); + + when(instance.hasError()).thenReturn(true); + + assertEquals(true, api.hasError(instance)); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/VideoRecordEventTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/VideoRecordEventTest.java new file mode 100644 index 000000000000..61a70efff9a7 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/VideoRecordEventTest.java @@ -0,0 +1,27 @@ +// Copyright 2013 The Flutter Authors +// 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.Mockito.mock; +import static org.mockito.Mockito.when; + +import androidx.camera.video.RecordingStats; +import androidx.camera.video.VideoRecordEvent; +import org.junit.Test; + +public class VideoRecordEventTest { + @Test + public void getRecordingStats_returnsExpectedValue() { + final PigeonApiVideoRecordEvent api = + new TestProxyApiRegistrar().getPigeonApiVideoRecordEvent(); + final VideoRecordEvent instance = mock(VideoRecordEvent.class); + final RecordingStats stats = mock(RecordingStats.class); + + when(instance.getRecordingStats()).thenReturn(stats); + + assertEquals(stats, api.getRecordingStats(instance)); + } +} diff --git a/packages/camera/camera_android_camerax/example/integration_test/integration_test.dart b/packages/camera/camera_android_camerax/example/integration_test/integration_test.dart index 89601306ddb4..1c78db4236b4 100644 --- a/packages/camera/camera_android_camerax/example/integration_test/integration_test.dart +++ b/packages/camera/camera_android_camerax/example/integration_test/integration_test.dart @@ -157,7 +157,46 @@ void main() { final int duration = videoController.value.duration.inMilliseconds; await videoController.dispose(); - expect(duration, lessThan(postStopTime)); + // Allow a 350ms tolerance around the 2s target to account for timer jitter, + // encoder latency, and frame quantization (typically 100-200ms on device, + // with extra margin to prevent flakes under heavy CI load). + // See https://github.com/flutter/flutter/issues/101785. + final int delta = (duration - 2000).abs(); + expect(delta, lessThan(350)); + expect(duration, lessThanOrEqualTo(postStopTime)); + await controller.dispose(); + }); + + testWidgets('Short video recording produces non-empty playable video', ( + WidgetTester tester, + ) async { + final List cameras = await availableCameras(); + if (cameras.isEmpty) { + return; + } + + final controller = CameraController( + cameras[0], + mediaSettings: const MediaSettings(resolutionPreset: ResolutionPreset.low), + ); + await controller.initialize(); + await controller.prepareForVideoRecording(); + + // A short recording (<300ms) previously caused ERROR_NO_VALID_DATA and 0-byte files + // if stop arrived before the first frame was encoded. + await controller.startVideoRecording(); + await Future.delayed(const Duration(milliseconds: 250)); + final XFile file = await controller.stopVideoRecording(); + + final int fileBytes = await file.length(); + expect(fileBytes, greaterThan(0)); + + final videoFile = File(file.path); + final videoController = VideoPlayerController.file(videoFile); + await videoController.initialize(); + expect(videoController.value.duration.inMilliseconds, greaterThan(0)); + await videoController.dispose(); + await controller.dispose(); }); testWidgets('Pause and resume video recording', (WidgetTester tester) async { 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 4d55205f565f..414d82a06c60 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 @@ -103,10 +103,12 @@ class AndroidCameraCameraX extends CameraPlatform { deviceOrientationChangedStreamController = StreamController.broadcast(); - /// Stream queue to pick up finalized viceo recording events in + /// Stream queue to pick up finalized video recording events in /// [stopVideoRecording]. - final StreamQueue videoRecordingEventStreamQueue = - StreamQueue(videoRecordingEventStreamController.stream); + @visibleForTesting + StreamQueue videoRecordingEventStreamQueue = StreamQueue( + videoRecordingEventStreamController.stream, + ); late final VideoRecordEventListener _videoRecordingEventListener = VideoRecordEventListener( onEvent: (_, VideoRecordEvent event) { @@ -1201,6 +1203,10 @@ class AndroidCameraCameraX extends CameraPlatform { // There is currently an active recording, so do not start a new one. return; } + unawaited(videoRecordingEventStreamQueue.cancel()); + videoRecordingEventStreamQueue = StreamQueue( + videoRecordingEventStreamController.stream, + ); final dynamic Function(CameraImageData)? streamCallback = options.streamCallback; if (streamCallback == null) { // For potential performance improvements, unbind imageAnalysis if not in use. @@ -1241,9 +1247,30 @@ class AndroidCameraCameraX extends CameraPlatform { onStreamedFrameAvailable(options.cameraId).listen(streamCallback); } - // Wait for video recording to start. + // Wait for video recording to start and for the first frame to be recorded. + await _waitForRecordingToStart(); + } + + /// Waits for the video recording to start and for the first frame to be recorded. + Future _waitForRecordingToStart() async { VideoRecordEvent event = await videoRecordingEventStreamQueue.next; - while (event is! VideoRecordEventStart) { + while (true) { + if (event is VideoRecordEventFinalize) { + final int errorCode = event.error; + recording = null; + pendingRecording = null; + await _unbindUseCaseFromLifecycle(videoCapture!); + throw CameraException( + 'videoRecordingFailed', + 'Video recording finalized before recording started with error code $errorCode.', + ); + } + if (event is VideoRecordEventStatus) { + final RecordingStats stats = await event.getRecordingStats(); + if (stats.numBytesRecorded > 0 || stats.recordedDurationNanos > 0) { + break; + } + } event = await videoRecordingEventStreamQueue.next; } } @@ -1284,6 +1311,15 @@ class AndroidCameraCameraX extends CameraPlatform { } await _unbindUseCaseFromLifecycle(videoCapture!); + + if (event.hasError) { + final int errorCode = event.error; + throw CameraException( + 'videoRecordingFailed', + 'Video recording finalized with error: $errorCode', + ); + } + final videoFile = XFile(videoOutputPath!); cameraEventStreamController.add(VideoRecordedEvent(cameraId, videoFile, /* duration */ null)); return videoFile; 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 334f1fa634b3..0dccdf09feff 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 @@ -426,8 +426,10 @@ class PigeonInstanceManager { CameraSize.pigeon_setUpMessageHandlers(pigeon_instanceManager: instanceManager); ResolutionInfo.pigeon_setUpMessageHandlers(pigeon_instanceManager: instanceManager); CameraIntegerRange.pigeon_setUpMessageHandlers(pigeon_instanceManager: instanceManager); + RecordingStats.pigeon_setUpMessageHandlers(pigeon_instanceManager: instanceManager); VideoRecordEvent.pigeon_setUpMessageHandlers(pigeon_instanceManager: instanceManager); VideoRecordEventStart.pigeon_setUpMessageHandlers(pigeon_instanceManager: instanceManager); + VideoRecordEventStatus.pigeon_setUpMessageHandlers(pigeon_instanceManager: instanceManager); VideoRecordEventFinalize.pigeon_setUpMessageHandlers(pigeon_instanceManager: instanceManager); MeteringPoint.pigeon_setUpMessageHandlers(pigeon_instanceManager: instanceManager); Observer.pigeon_setUpMessageHandlers(pigeon_instanceManager: instanceManager); @@ -1367,6 +1369,87 @@ class CameraIntegerRange extends PigeonInternalProxyApiBaseClass { } } +/// RecordingStats provides recording statistics such as duration and bytes recorded. +/// +/// See https://developer.android.com/reference/androidx/camera/video/RecordingStats. +class RecordingStats extends PigeonInternalProxyApiBaseClass { + /// Constructs [RecordingStats] without creating the associated native object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies for an [PigeonInstanceManager]. + @protected + RecordingStats.pigeon_detached({ + super.pigeon_binaryMessenger, + super.pigeon_instanceManager, + required this.recordedDurationNanos, + required this.numBytesRecorded, + }); + + /// The recorded duration in nanoseconds. + final int recordedDurationNanos; + + /// The number of bytes recorded. + final int numBytesRecorded; + + static void pigeon_setUpMessageHandlers({ + bool pigeon_clearHandlers = false, + BinaryMessenger? pigeon_binaryMessenger, + PigeonInstanceManager? pigeon_instanceManager, + RecordingStats Function(int recordedDurationNanos, int numBytesRecorded)? pigeon_newInstance, + }) { + final _PigeonInternalProxyApiBaseCodec pigeonChannelCodec = _PigeonInternalProxyApiBaseCodec( + pigeon_instanceManager ?? PigeonInstanceManager.instance, + ); + final BinaryMessenger? binaryMessenger = pigeon_binaryMessenger; + { + final pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.camera_android_camerax.RecordingStats.pigeon_newInstance', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); + if (pigeon_clearHandlers) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + final List args = message! as List; + final int arg_pigeon_instanceIdentifier = args[0]! as int; + final int arg_recordedDurationNanos = args[1]! as int; + final int arg_numBytesRecorded = args[2]! as int; + try { + (pigeon_instanceManager ?? PigeonInstanceManager.instance).addHostCreatedInstance( + pigeon_newInstance?.call(arg_recordedDurationNanos, arg_numBytesRecorded) ?? + RecordingStats.pigeon_detached( + pigeon_binaryMessenger: pigeon_binaryMessenger, + pigeon_instanceManager: pigeon_instanceManager, + recordedDurationNanos: arg_recordedDurationNanos, + numBytesRecorded: arg_numBytesRecorded, + ), + arg_pigeon_instanceIdentifier, + ); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); + } + }); + } + } + } + + @override + RecordingStats pigeon_copy() { + return RecordingStats.pigeon_detached( + pigeon_binaryMessenger: pigeon_binaryMessenger, + pigeon_instanceManager: pigeon_instanceManager, + recordedDurationNanos: recordedDurationNanos, + numBytesRecorded: numBytesRecorded, + ); + } +} + /// VideoRecordEvent is used to report video recording events and status. /// /// See https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent. @@ -1378,6 +1461,9 @@ class VideoRecordEvent extends PigeonInternalProxyApiBaseClass { @protected VideoRecordEvent.pigeon_detached({super.pigeon_binaryMessenger, super.pigeon_instanceManager}); + late final _PigeonInternalProxyApiBaseCodec _pigeonVar_codecVideoRecordEvent = + _PigeonInternalProxyApiBaseCodec(pigeon_instanceManager); + static void pigeon_setUpMessageHandlers({ bool pigeon_clearHandlers = false, BinaryMessenger? pigeon_binaryMessenger, @@ -1422,6 +1508,28 @@ class VideoRecordEvent extends PigeonInternalProxyApiBaseClass { } } + /// The recording statistics for this event. + Future getRecordingStats() async { + final _PigeonInternalProxyApiBaseCodec pigeonChannelCodec = _pigeonVar_codecVideoRecordEvent; + final BinaryMessenger? pigeonVar_binaryMessenger = pigeon_binaryMessenger; + const pigeonVar_channelName = + 'dev.flutter.pigeon.camera_android_camerax.VideoRecordEvent.getRecordingStats'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([this]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); + return pigeonVar_replyValue! as RecordingStats; + } + @override VideoRecordEvent pigeon_copy() { return VideoRecordEvent.pigeon_detached( @@ -1498,6 +1606,73 @@ class VideoRecordEventStart extends VideoRecordEvent { } } +/// Indicates recording status updates. +/// +/// See https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent.Status. +class VideoRecordEventStatus extends VideoRecordEvent { + /// Constructs [VideoRecordEventStatus] without creating the associated native object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies for an [PigeonInstanceManager]. + @protected + VideoRecordEventStatus.pigeon_detached({ + super.pigeon_binaryMessenger, + super.pigeon_instanceManager, + }) : super.pigeon_detached(); + + static void pigeon_setUpMessageHandlers({ + bool pigeon_clearHandlers = false, + BinaryMessenger? pigeon_binaryMessenger, + PigeonInstanceManager? pigeon_instanceManager, + VideoRecordEventStatus Function()? pigeon_newInstance, + }) { + final _PigeonInternalProxyApiBaseCodec pigeonChannelCodec = _PigeonInternalProxyApiBaseCodec( + pigeon_instanceManager ?? PigeonInstanceManager.instance, + ); + final BinaryMessenger? binaryMessenger = pigeon_binaryMessenger; + { + final pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.camera_android_camerax.VideoRecordEventStatus.pigeon_newInstance', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); + if (pigeon_clearHandlers) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + final List args = message! as List; + final int arg_pigeon_instanceIdentifier = args[0]! as int; + try { + (pigeon_instanceManager ?? PigeonInstanceManager.instance).addHostCreatedInstance( + pigeon_newInstance?.call() ?? + VideoRecordEventStatus.pigeon_detached( + pigeon_binaryMessenger: pigeon_binaryMessenger, + pigeon_instanceManager: pigeon_instanceManager, + ), + arg_pigeon_instanceIdentifier, + ); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); + } + }); + } + } + } + + @override + VideoRecordEventStatus pigeon_copy() { + return VideoRecordEventStatus.pigeon_detached( + pigeon_binaryMessenger: pigeon_binaryMessenger, + pigeon_instanceManager: pigeon_instanceManager, + ); + } +} + /// Indicates the finalization of recording. /// /// See https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent.Finalize. @@ -1510,13 +1685,25 @@ class VideoRecordEventFinalize extends VideoRecordEvent { VideoRecordEventFinalize.pigeon_detached({ super.pigeon_binaryMessenger, super.pigeon_instanceManager, + required this.error, + required this.hasError, }) : super.pigeon_detached(); + /// The error code of the finalization. + /// + /// See https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent.Finalize#getError(). + final int error; + + /// Whether the recording finalized with an error. + /// + /// See https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent.Finalize#hasError(). + final bool hasError; + static void pigeon_setUpMessageHandlers({ bool pigeon_clearHandlers = false, BinaryMessenger? pigeon_binaryMessenger, PigeonInstanceManager? pigeon_instanceManager, - VideoRecordEventFinalize Function()? pigeon_newInstance, + VideoRecordEventFinalize Function(int error, bool hasError)? pigeon_newInstance, }) { final _PigeonInternalProxyApiBaseCodec pigeonChannelCodec = _PigeonInternalProxyApiBaseCodec( pigeon_instanceManager ?? PigeonInstanceManager.instance, @@ -1534,12 +1721,16 @@ class VideoRecordEventFinalize extends VideoRecordEvent { pigeonVar_channel.setMessageHandler((Object? message) async { final List args = message! as List; final int arg_pigeon_instanceIdentifier = args[0]! as int; + final int arg_error = args[1]! as int; + final bool arg_hasError = args[2]! as bool; try { (pigeon_instanceManager ?? PigeonInstanceManager.instance).addHostCreatedInstance( - pigeon_newInstance?.call() ?? + pigeon_newInstance?.call(arg_error, arg_hasError) ?? VideoRecordEventFinalize.pigeon_detached( pigeon_binaryMessenger: pigeon_binaryMessenger, pigeon_instanceManager: pigeon_instanceManager, + error: arg_error, + hasError: arg_hasError, ), arg_pigeon_instanceIdentifier, ); @@ -1561,6 +1752,8 @@ class VideoRecordEventFinalize extends VideoRecordEvent { return VideoRecordEventFinalize.pigeon_detached( pigeon_binaryMessenger: pigeon_binaryMessenger, pigeon_instanceManager: pigeon_instanceManager, + error: error, + hasError: hasError, ); } } diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart index 0db222d8382c..e4c9d100f57f 100644 --- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart +++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart @@ -152,13 +152,30 @@ enum VideoQuality { highest, } +/// RecordingStats provides recording statistics such as duration and bytes recorded. +/// +/// See https://developer.android.com/reference/androidx/camera/video/RecordingStats. +@ProxyApi( + kotlinOptions: KotlinProxyApiOptions(fullClassName: 'androidx.camera.video.RecordingStats'), +) +abstract class RecordingStats { + /// The recorded duration in nanoseconds. + late int recordedDurationNanos; + + /// The number of bytes recorded. + late int numBytesRecorded; +} + /// VideoRecordEvent is used to report video recording events and status. /// /// See https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent. @ProxyApi( kotlinOptions: KotlinProxyApiOptions(fullClassName: 'androidx.camera.video.VideoRecordEvent'), ) -abstract class VideoRecordEvent {} +abstract class VideoRecordEvent { + /// The recording statistics for this event. + RecordingStats getRecordingStats(); +} /// Indicates the start of recording. /// @@ -170,6 +187,16 @@ abstract class VideoRecordEvent {} ) abstract class VideoRecordEventStart extends VideoRecordEvent {} +/// Indicates recording status updates. +/// +/// See https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent.Status. +@ProxyApi( + kotlinOptions: KotlinProxyApiOptions( + fullClassName: 'androidx.camera.video.VideoRecordEvent.Status', + ), +) +abstract class VideoRecordEventStatus extends VideoRecordEvent {} + /// Indicates the finalization of recording. /// /// See https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent.Finalize. @@ -178,7 +205,17 @@ abstract class VideoRecordEventStart extends VideoRecordEvent {} fullClassName: 'androidx.camera.video.VideoRecordEvent.Finalize', ), ) -abstract class VideoRecordEventFinalize extends VideoRecordEvent {} +abstract class VideoRecordEventFinalize extends VideoRecordEvent { + /// The error code of the finalization. + /// + /// See https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent.Finalize#getError(). + late int error; + + /// Whether the recording finalized with an error. + /// + /// See https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent.Finalize#hasError(). + late bool hasError; +} /// A MeteringPoint is used to specify a region which can then be converted to /// sensor coordinate system for focus and metering purpose. diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 7c111b9e1749..4f0be9473f49 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_android_camerax description: Android implementation of the camera plugin using the CameraX library. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.7.6 +version: 0.7.6+1 environment: sdk: ^3.12.0 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 d5f572ee3ae8..2a1618d5ead9 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 @@ -49,8 +49,12 @@ import 'android_camera_camerax_test.mocks.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), MockSpec(), MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), MockSpec(), ]) @GenerateMocks( @@ -102,6 +106,34 @@ void main() { return cameraClosingEventSent && cameraErrorSent; } + /// Helper method to simulate video recording start events. + void emitRecordingStarted() { + final mockStatusEvent = MockVideoRecordEventStatus(); + final stats = RecordingStats.pigeon_detached( + recordedDurationNanos: 1000000, + numBytesRecorded: 100, + ); + when(mockStatusEvent.getRecordingStats()).thenAnswer((_) async => stats); + + scheduleMicrotask(() { + AndroidCameraCameraX.videoRecordingEventStreamController.add( + VideoRecordEventStart.pigeon_detached(), + ); + AndroidCameraCameraX.videoRecordingEventStreamController.add(mockStatusEvent); + }); + } + + /// Helper method to simulate video recording finalize events. + void emitRecordingFinalized({bool hasError = false, int error = 0}) { + final mockFinalizeEvent = MockVideoRecordEventFinalize(); + when(mockFinalizeEvent.hasError).thenReturn(hasError); + when(mockFinalizeEvent.error).thenReturn(error); + + scheduleMicrotask(() { + AndroidCameraCameraX.videoRecordingEventStreamController.add(mockFinalizeEvent); + }); + } + /// Set up testing functionality related to the configuration /// of CameraX UseCases. void setUpOverridesForTestingUseCaseConfiguration( @@ -2297,9 +2329,7 @@ void main() { ).thenAnswer((_) async => InfoSupportedHardwareLevel.limited); // Simulate video recording being started so startVideoRecording completes. - AndroidCameraCameraX.videoRecordingEventStreamController.add( - VideoRecordEventStart.pigeon_detached(), - ); + emitRecordingStarted(); await camera.startVideoCapturing(const VideoCaptureOptions(cameraId)); @@ -2402,9 +2432,7 @@ void main() { ).thenAnswer((_) async => InfoSupportedHardwareLevel.limited); // Simulate video recording being started so startVideoRecording completes. - AndroidCameraCameraX.videoRecordingEventStreamController.add( - VideoRecordEventStart.pigeon_detached(), - ); + emitRecordingStarted(); await camera.startVideoCapturing(const VideoCaptureOptions(cameraId)); @@ -2491,9 +2519,7 @@ void main() { ).thenAnswer((_) async => InfoSupportedHardwareLevel.limited); // Simulate video recording being started so startVideoRecording completes. - AndroidCameraCameraX.videoRecordingEventStreamController.add( - VideoRecordEventStart.pigeon_detached(), - ); + emitRecordingStarted(); await camera.startVideoCapturing( const VideoCaptureOptions(1, videoOutputPath: videoOutputPath), @@ -2503,6 +2529,220 @@ void main() { expect(camera.videoOutputPath, videoOutputPath); }); + test( + 'startVideoCapturing throws CameraException if VideoRecordEventFinalize received before starting', + () async { + final camera = AndroidCameraCameraX(); + final mockProcessCameraProvider = MockProcessCameraProvider(); + final mockCamera = MockCamera(); + final mockCameraInfo = MockCameraInfo(); + final mockCameraControl = MockCameraControl(); + final mockCamera2CameraInfo = MockCamera2CameraInfo(); + final mockRecorder = MockRecorder(); + final mockVideoCapture = MockVideoCapture(); + final mockPendingRecording = MockPendingRecording(); + final mockRecording = MockRecording(); + final mockImageAnalysis = MockImageAnalysis(); + const cameraId = 0; + + camera.processCameraProvider = mockProcessCameraProvider; + camera.cameraSelector = MockCameraSelector(); + camera.camera = mockCamera; + camera.cameraInfo = mockCameraInfo; + camera.cameraControl = mockCameraControl; + camera.recorder = mockRecorder; + camera.videoCapture = mockVideoCapture; + camera.imageAnalysis = mockImageAnalysis; + camera.enableRecordingAudio = false; + + PigeonOverrides.camera2CameraInfo_from = ({required dynamic cameraInfo}) => + mockCamera2CameraInfo; + PigeonOverrides.videoRecordEventListener_new = + ({required void Function(VideoRecordEventListener, VideoRecordEvent) onEvent}) { + return VideoRecordEventListener.pigeon_detached(onEvent: onEvent); + }; + PigeonOverrides.cameraCharacteristics_infoSupportedHardwareLevel = + MockCameraCharacteristicsKey(); + when( + mockCamera2CameraInfo.getCameraCharacteristic(any), + ).thenAnswer((_) async => InfoSupportedHardwareLevel.limited); + + when(mockRecorder.prepareRecording(any)).thenAnswer((_) async => mockPendingRecording); + when( + mockPendingRecording.withAudioEnabled(any), + ).thenAnswer((_) async => mockPendingRecording); + when( + mockPendingRecording.asPersistentRecording(), + ).thenAnswer((_) async => mockPendingRecording); + when(mockPendingRecording.start(any)).thenAnswer((_) async => mockRecording); + when(mockProcessCameraProvider.isBound(mockVideoCapture)).thenAnswer((_) async => true); + when(mockProcessCameraProvider.isBound(mockImageAnalysis)).thenAnswer((_) async => false); + + emitRecordingFinalized(hasError: true, error: 8); + + await expectLater( + () async => camera.startVideoCapturing( + const VideoCaptureOptions(cameraId, videoOutputPath: '/test/output/path'), + ), + throwsA( + isA().having( + (e) => e.description, + 'description', + contains('error code 8'), + ), + ), + ); + expect(camera.recording, isNull); + expect(camera.pendingRecording, isNull); + verify(mockProcessCameraProvider.unbind([mockVideoCapture])); + }, + ); + + test( + 'startVideoCapturing ignores initial zero-byte status events and waits until first frame recorded', + () async { + final camera = AndroidCameraCameraX(); + final mockProcessCameraProvider = MockProcessCameraProvider(); + final mockCamera = MockCamera(); + final mockCameraInfo = MockCameraInfo(); + final mockCameraControl = MockCameraControl(); + final mockCamera2CameraInfo = MockCamera2CameraInfo(); + final mockRecorder = MockRecorder(); + final mockVideoCapture = MockVideoCapture(); + final mockPendingRecording = MockPendingRecording(); + final mockRecording = MockRecording(); + final mockImageAnalysis = MockImageAnalysis(); + const cameraId = 0; + + camera.processCameraProvider = mockProcessCameraProvider; + camera.cameraSelector = MockCameraSelector(); + camera.camera = mockCamera; + camera.cameraInfo = mockCameraInfo; + camera.cameraControl = mockCameraControl; + camera.recorder = mockRecorder; + camera.videoCapture = mockVideoCapture; + camera.imageAnalysis = mockImageAnalysis; + camera.enableRecordingAudio = false; + + PigeonOverrides.camera2CameraInfo_from = ({required dynamic cameraInfo}) => + mockCamera2CameraInfo; + PigeonOverrides.videoRecordEventListener_new = + ({required void Function(VideoRecordEventListener, VideoRecordEvent) onEvent}) { + return VideoRecordEventListener.pigeon_detached(onEvent: onEvent); + }; + PigeonOverrides.cameraCharacteristics_infoSupportedHardwareLevel = + MockCameraCharacteristicsKey(); + when( + mockCamera2CameraInfo.getCameraCharacteristic(any), + ).thenAnswer((_) async => InfoSupportedHardwareLevel.limited); + + when(mockRecorder.prepareRecording(any)).thenAnswer((_) async => mockPendingRecording); + when( + mockPendingRecording.withAudioEnabled(any), + ).thenAnswer((_) async => mockPendingRecording); + when( + mockPendingRecording.asPersistentRecording(), + ).thenAnswer((_) async => mockPendingRecording); + when(mockPendingRecording.start(any)).thenAnswer((_) async => mockRecording); + when(mockProcessCameraProvider.isBound(mockVideoCapture)).thenAnswer((_) async => true); + when(mockProcessCameraProvider.isBound(mockImageAnalysis)).thenAnswer((_) async => false); + + final mockZeroStatus = MockVideoRecordEventStatus(); + final zeroStats = RecordingStats.pigeon_detached( + recordedDurationNanos: 0, + numBytesRecorded: 0, + ); + when(mockZeroStatus.getRecordingStats()).thenAnswer((_) async => zeroStats); + + final mockDataStatus = MockVideoRecordEventStatus(); + final dataStats = RecordingStats.pigeon_detached( + recordedDurationNanos: 50000000, + numBytesRecorded: 1024, + ); + when(mockDataStatus.getRecordingStats()).thenAnswer((_) async => dataStats); + + scheduleMicrotask(() { + AndroidCameraCameraX.videoRecordingEventStreamController.add( + VideoRecordEventStart.pigeon_detached(), + ); + AndroidCameraCameraX.videoRecordingEventStreamController.add(mockZeroStatus); + AndroidCameraCameraX.videoRecordingEventStreamController.add(mockDataStatus); + }); + + await camera.startVideoCapturing( + const VideoCaptureOptions(cameraId, videoOutputPath: '/test/output/path'), + ); + + expect(camera.recording, equals(mockRecording)); + }, + ); + + test( + 'startVideoCapturing recreates stream queue and ignores leftover events from previous recording', + () async { + final camera = AndroidCameraCameraX(); + final mockProcessCameraProvider = MockProcessCameraProvider(); + final mockCamera = MockCamera(); + final mockCameraInfo = MockCameraInfo(); + final mockCameraControl = MockCameraControl(); + final mockCamera2CameraInfo = MockCamera2CameraInfo(); + final mockRecorder = MockRecorder(); + final mockVideoCapture = MockVideoCapture(); + final mockPendingRecording = MockPendingRecording(); + final mockRecording = MockRecording(); + final mockImageAnalysis = MockImageAnalysis(); + const cameraId = 0; + + camera.processCameraProvider = mockProcessCameraProvider; + camera.cameraSelector = MockCameraSelector(); + camera.camera = mockCamera; + camera.cameraInfo = mockCameraInfo; + camera.cameraControl = mockCameraControl; + camera.recorder = mockRecorder; + camera.videoCapture = mockVideoCapture; + camera.imageAnalysis = mockImageAnalysis; + camera.enableRecordingAudio = false; + + PigeonOverrides.camera2CameraInfo_from = ({required dynamic cameraInfo}) => + mockCamera2CameraInfo; + PigeonOverrides.videoRecordEventListener_new = + ({required void Function(VideoRecordEventListener, VideoRecordEvent) onEvent}) { + return VideoRecordEventListener.pigeon_detached(onEvent: onEvent); + }; + PigeonOverrides.cameraCharacteristics_infoSupportedHardwareLevel = + MockCameraCharacteristicsKey(); + when( + mockCamera2CameraInfo.getCameraCharacteristic(any), + ).thenAnswer((_) async => InfoSupportedHardwareLevel.limited); + + when(mockRecorder.prepareRecording(any)).thenAnswer((_) async => mockPendingRecording); + when( + mockPendingRecording.withAudioEnabled(any), + ).thenAnswer((_) async => mockPendingRecording); + when( + mockPendingRecording.asPersistentRecording(), + ).thenAnswer((_) async => mockPendingRecording); + when(mockPendingRecording.start(any)).thenAnswer((_) async => mockRecording); + when(mockProcessCameraProvider.isBound(mockVideoCapture)).thenAnswer((_) async => true); + when(mockProcessCameraProvider.isBound(mockImageAnalysis)).thenAnswer((_) async => false); + + // Simulate a leftover finalize event from a previous session that ended + // without calling stopVideoRecording(). + final mockLeftoverFinalize = MockVideoRecordEventFinalize(); + when(mockLeftoverFinalize.error).thenReturn(8); + AndroidCameraCameraX.videoRecordingEventStreamController.add(mockLeftoverFinalize); + + // Simulate the new recording session emitting its own start and status events. + emitRecordingStarted(); + + await camera.startVideoCapturing( + const VideoCaptureOptions(cameraId, videoOutputPath: '/test/output/path'), + ); + + expect(camera.recording, equals(mockRecording)); + }, + ); + test('startVideoCapturing called with stream options starts image streaming', () async { // Set up mocks and constants. final camera = AndroidCameraCameraX(); @@ -2587,9 +2827,7 @@ void main() { ).thenAnswer((_) async => InfoSupportedHardwareLevel.level3); // Simulate video recording being started so startVideoRecording completes. - AndroidCameraCameraX.videoRecordingEventStreamController.add( - VideoRecordEventStart.pigeon_detached(), - ); + emitRecordingStarted(); await camera.startVideoCapturing(videoCaptureOptions); @@ -2679,9 +2917,7 @@ void main() { ).thenAnswer((_) async => InfoSupportedHardwareLevel.limited); // Simulate video recording being started so startVideoRecording completes. - AndroidCameraCameraX.videoRecordingEventStreamController.add( - VideoRecordEventStart.pigeon_detached(), - ); + emitRecordingStarted(); // Orientation is unlocked and plugin does not need to set default target // rotation manually. @@ -2690,9 +2926,7 @@ void main() { verifyNever(mockVideoCapture.setTargetRotation(any)); // Simulate video recording being started so startVideoRecording completes. - AndroidCameraCameraX.videoRecordingEventStreamController.add( - VideoRecordEventStart.pigeon_detached(), - ); + emitRecordingStarted(); // Orientation is locked and plugin does not need to set default target // rotation manually. @@ -2702,9 +2936,7 @@ void main() { verifyNever(mockVideoCapture.setTargetRotation(any)); // Simulate video recording being started so startVideoRecording completes. - AndroidCameraCameraX.videoRecordingEventStreamController.add( - VideoRecordEventStart.pigeon_detached(), - ); + emitRecordingStarted(); // Orientation is locked and plugin does need to set default target // rotation manually. @@ -2715,9 +2947,7 @@ void main() { verifyNever(mockVideoCapture.setTargetRotation(any)); // Simulate video recording being started so startVideoRecording completes. - AndroidCameraCameraX.videoRecordingEventStreamController.add( - VideoRecordEventStart.pigeon_detached(), - ); + emitRecordingStarted(); // Orientation is unlocked and plugin does need to set default target // rotation manually. @@ -2770,9 +3000,7 @@ void main() { when(camera.processCameraProvider!.isBound(videoCapture)).thenAnswer((_) async => true); // Simulate video recording being finalized so stopVideoRecording completes. - AndroidCameraCameraX.videoRecordingEventStreamController.add( - VideoRecordEventFinalize.pigeon_detached(), - ); + emitRecordingFinalized(); final XFile file = await camera.stopVideoRecording(0); expect(file.path, videoOutputPath); @@ -2813,9 +3041,7 @@ void main() { await expectLater(() async { // Simulate video recording being finalized so stopVideoRecording completes. - AndroidCameraCameraX.videoRecordingEventStreamController.add( - VideoRecordEventFinalize.pigeon_detached(), - ); + emitRecordingFinalized(); await camera.stopVideoRecording(0); }, throwsA(isA())); expect(camera.recording, null); @@ -2836,9 +3062,7 @@ void main() { camera.videoOutputPath = videoOutputPath; // Simulate video recording being finalized so stopVideoRecording completes. - AndroidCameraCameraX.videoRecordingEventStreamController.add( - VideoRecordEventFinalize.pigeon_detached(), - ); + emitRecordingFinalized(); final XFile file = await camera.stopVideoRecording(0); expect(file.path, videoOutputPath); @@ -2865,9 +3089,7 @@ void main() { when(camera.processCameraProvider!.isBound(videoCapture)).thenAnswer((_) async => true); // Simulate video recording being finalized so stopVideoRecording completes. - AndroidCameraCameraX.videoRecordingEventStreamController.add( - VideoRecordEventFinalize.pigeon_detached(), - ); + emitRecordingFinalized(); await camera.stopVideoRecording(90); verify(processCameraProvider.unbind([videoCapture])); @@ -2877,6 +3099,31 @@ void main() { verifyNoMoreInteractions(recording); }); + test('stopVideoRecording throws CameraException if recording finalized with error', () async { + final camera = AndroidCameraCameraX(); + final recording = MockRecording(); + final processCameraProvider = MockProcessCameraProvider(); + final videoCapture = MockVideoCapture(); + const videoOutputPath = '/test/output/path'; + + camera.processCameraProvider = processCameraProvider; + camera.recording = recording; + camera.videoCapture = videoCapture; + camera.videoOutputPath = videoOutputPath; + + when(camera.processCameraProvider!.isBound(videoCapture)).thenAnswer((_) async => true); + + emitRecordingFinalized(hasError: true, error: 8); + + await expectLater( + () async => camera.stopVideoRecording(0), + throwsA( + isA().having((e) => e.description, 'description', contains('error: 8')), + ), + ); + verify(processCameraProvider.unbind([videoCapture])); + }); + test('setDescriptionWhileRecording changes the camera description', () async { final camera = AndroidCameraCameraX(); final mockRecording = MockRecording(); @@ -3042,11 +3289,6 @@ void main() { when(newMockCamera.cameraControl).thenReturn(mockCameraControl); when(mockCameraInfo.getCameraState()).thenAnswer((_) async => newMockLiveCameraState); - // Simulate video recording being started so startVideoRecording completes. - AndroidCameraCameraX.videoRecordingEventStreamController.add( - VideoRecordEventStart.pigeon_detached(), - ); - await camera.availableCameras(); final int flutterSurfaceTextureId = await camera.createCameraWithSettings( @@ -3055,6 +3297,9 @@ void main() { ); await camera.initializeCamera(flutterSurfaceTextureId); + // Simulate video recording being started so startVideoRecording completes. + emitRecordingStarted(); + await camera.startVideoCapturing(VideoCaptureOptions(flutterSurfaceTextureId)); await camera.setDescriptionWhileRecording(testFrontCameraDescription); @@ -3247,11 +3492,6 @@ void main() { when(mockCameraInfo.getCameraState()).thenAnswer((_) async => MockLiveCameraState()); when(mockCamera.cameraControl).thenAnswer((_) => mockCameraControl); - // Simulate video recording being started so startVideoRecording completes. - AndroidCameraCameraX.videoRecordingEventStreamController.add( - VideoRecordEventStart.pigeon_detached(), - ); - await camera.availableCameras(); final int flutterSurfaceTextureId = await camera.createCameraWithSettings( @@ -3260,6 +3500,9 @@ void main() { ); await camera.initializeCamera(flutterSurfaceTextureId); + // Simulate video recording being started so startVideoRecording completes. + emitRecordingStarted(); + await camera.startVideoCapturing(VideoCaptureOptions(flutterSurfaceTextureId)); // pause the preview @@ -6108,9 +6351,7 @@ void main() { ).thenAnswer((_) async => InfoSupportedHardwareLevel.level3); // Simulate video recording being started so startVideoRecording completes. - AndroidCameraCameraX.videoRecordingEventStreamController.add( - VideoRecordEventStart.pigeon_detached(), - ); + emitRecordingStarted(); await camera.startVideoCapturing(const VideoCaptureOptions(cameraId)); 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 ac63f5dba4a5..088111797362 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 @@ -209,22 +209,43 @@ class _FakeResolutionStrategy_37 extends _i1.SmartFake implements _i2.Resolution : super(parent, parentInvocation); } -class _FakeSystemServicesManager_38 extends _i1.SmartFake implements _i2.SystemServicesManager { - _FakeSystemServicesManager_38(Object parent, Invocation parentInvocation) +class _FakeRecordingStats_38 extends _i1.SmartFake implements _i2.RecordingStats { + _FakeRecordingStats_38(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); } -class _FakeVideoOutput_39 extends _i1.SmartFake implements _i2.VideoOutput { - _FakeVideoOutput_39(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +class _FakeSystemServicesManager_39 extends _i1.SmartFake implements _i2.SystemServicesManager { + _FakeSystemServicesManager_39(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeVideoOutput_40 extends _i1.SmartFake implements _i2.VideoOutput { + _FakeVideoOutput_40(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); } -class _FakeVideoCapture_40 extends _i1.SmartFake implements _i2.VideoCapture { - _FakeVideoCapture_40(Object parent, Invocation parentInvocation) +class _FakeVideoCapture_41 extends _i1.SmartFake implements _i2.VideoCapture { + _FakeVideoCapture_41(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); } -class _FakeZoomState_41 extends _i1.SmartFake implements _i2.ZoomState { - _FakeZoomState_41(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +class _FakeVideoRecordEventFinalize_42 extends _i1.SmartFake + implements _i2.VideoRecordEventFinalize { + _FakeVideoRecordEventFinalize_42(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeVideoRecordEventStart_43 extends _i1.SmartFake implements _i2.VideoRecordEventStart { + _FakeVideoRecordEventStart_43(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeVideoRecordEventStatus_44 extends _i1.SmartFake implements _i2.VideoRecordEventStatus { + _FakeVideoRecordEventStatus_44(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeZoomState_45 extends _i1.SmartFake implements _i2.ZoomState { + _FakeZoomState_45(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); } /// A class which mocks [Analyzer]. @@ -2008,6 +2029,56 @@ class MockRecording extends _i1.Mock implements _i2.Recording { as _i2.Recording); } +/// A class which mocks [RecordingStats]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockRecordingStats extends _i1.Mock implements _i2.RecordingStats { + @override + int get recordedDurationNanos => + (super.noSuchMethod( + Invocation.getter(#recordedDurationNanos), + returnValue: 0, + returnValueForMissingStub: 0, + ) + as int); + + @override + int get numBytesRecorded => + (super.noSuchMethod( + Invocation.getter(#numBytesRecorded), + returnValue: 0, + returnValueForMissingStub: 0, + ) + as int); + + @override + _i2.PigeonInstanceManager get pigeon_instanceManager => + (super.noSuchMethod( + Invocation.getter(#pigeon_instanceManager), + returnValue: _FakePigeonInstanceManager_0( + this, + Invocation.getter(#pigeon_instanceManager), + ), + returnValueForMissingStub: _FakePigeonInstanceManager_0( + this, + Invocation.getter(#pigeon_instanceManager), + ), + ) + as _i2.PigeonInstanceManager); + + @override + _i2.RecordingStats pigeon_copy() => + (super.noSuchMethod( + Invocation.method(#pigeon_copy, []), + returnValue: _FakeRecordingStats_38(this, Invocation.method(#pigeon_copy, [])), + returnValueForMissingStub: _FakeRecordingStats_38( + this, + Invocation.method(#pigeon_copy, []), + ), + ) + as _i2.RecordingStats); +} + /// A class which mocks [SystemServicesManager]. /// /// See the documentation for Mockito's code generation for more information. @@ -2063,8 +2134,8 @@ class MockSystemServicesManager extends _i1.Mock implements _i2.SystemServicesMa _i2.SystemServicesManager pigeon_copy() => (super.noSuchMethod( Invocation.method(#pigeon_copy, []), - returnValue: _FakeSystemServicesManager_38(this, Invocation.method(#pigeon_copy, [])), - returnValueForMissingStub: _FakeSystemServicesManager_38( + returnValue: _FakeSystemServicesManager_39(this, Invocation.method(#pigeon_copy, [])), + returnValueForMissingStub: _FakeSystemServicesManager_39( this, Invocation.method(#pigeon_copy, []), ), @@ -2096,10 +2167,10 @@ class MockVideoCapture extends _i1.Mock implements _i2.VideoCapture { (super.noSuchMethod( Invocation.method(#getOutput, []), returnValue: _i5.Future<_i2.VideoOutput>.value( - _FakeVideoOutput_39(this, Invocation.method(#getOutput, [])), + _FakeVideoOutput_40(this, Invocation.method(#getOutput, [])), ), returnValueForMissingStub: _i5.Future<_i2.VideoOutput>.value( - _FakeVideoOutput_39(this, Invocation.method(#getOutput, [])), + _FakeVideoOutput_40(this, Invocation.method(#getOutput, [])), ), ) as _i5.Future<_i2.VideoOutput>); @@ -2117,8 +2188,8 @@ class MockVideoCapture extends _i1.Mock implements _i2.VideoCapture { _i2.VideoCapture pigeon_copy() => (super.noSuchMethod( Invocation.method(#pigeon_copy, []), - returnValue: _FakeVideoCapture_40(this, Invocation.method(#pigeon_copy, [])), - returnValueForMissingStub: _FakeVideoCapture_40( + returnValue: _FakeVideoCapture_41(this, Invocation.method(#pigeon_copy, [])), + returnValueForMissingStub: _FakeVideoCapture_41( this, Invocation.method(#pigeon_copy, []), ), @@ -2126,6 +2197,158 @@ class MockVideoCapture extends _i1.Mock implements _i2.VideoCapture { as _i2.VideoCapture); } +/// A class which mocks [VideoRecordEventFinalize]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockVideoRecordEventFinalize extends _i1.Mock implements _i2.VideoRecordEventFinalize { + @override + int get error => + (super.noSuchMethod(Invocation.getter(#error), returnValue: 0, returnValueForMissingStub: 0) + as int); + + @override + bool get hasError => + (super.noSuchMethod( + Invocation.getter(#hasError), + returnValue: false, + returnValueForMissingStub: false, + ) + as bool); + + @override + _i2.PigeonInstanceManager get pigeon_instanceManager => + (super.noSuchMethod( + Invocation.getter(#pigeon_instanceManager), + returnValue: _FakePigeonInstanceManager_0( + this, + Invocation.getter(#pigeon_instanceManager), + ), + returnValueForMissingStub: _FakePigeonInstanceManager_0( + this, + Invocation.getter(#pigeon_instanceManager), + ), + ) + as _i2.PigeonInstanceManager); + + @override + _i2.VideoRecordEventFinalize pigeon_copy() => + (super.noSuchMethod( + Invocation.method(#pigeon_copy, []), + returnValue: _FakeVideoRecordEventFinalize_42( + this, + Invocation.method(#pigeon_copy, []), + ), + returnValueForMissingStub: _FakeVideoRecordEventFinalize_42( + this, + Invocation.method(#pigeon_copy, []), + ), + ) + as _i2.VideoRecordEventFinalize); + + @override + _i5.Future<_i2.RecordingStats> getRecordingStats() => + (super.noSuchMethod( + Invocation.method(#getRecordingStats, []), + returnValue: _i5.Future<_i2.RecordingStats>.value( + _FakeRecordingStats_38(this, Invocation.method(#getRecordingStats, [])), + ), + returnValueForMissingStub: _i5.Future<_i2.RecordingStats>.value( + _FakeRecordingStats_38(this, Invocation.method(#getRecordingStats, [])), + ), + ) + as _i5.Future<_i2.RecordingStats>); +} + +/// A class which mocks [VideoRecordEventStart]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockVideoRecordEventStart extends _i1.Mock implements _i2.VideoRecordEventStart { + @override + _i2.PigeonInstanceManager get pigeon_instanceManager => + (super.noSuchMethod( + Invocation.getter(#pigeon_instanceManager), + returnValue: _FakePigeonInstanceManager_0( + this, + Invocation.getter(#pigeon_instanceManager), + ), + returnValueForMissingStub: _FakePigeonInstanceManager_0( + this, + Invocation.getter(#pigeon_instanceManager), + ), + ) + as _i2.PigeonInstanceManager); + + @override + _i2.VideoRecordEventStart pigeon_copy() => + (super.noSuchMethod( + Invocation.method(#pigeon_copy, []), + returnValue: _FakeVideoRecordEventStart_43(this, Invocation.method(#pigeon_copy, [])), + returnValueForMissingStub: _FakeVideoRecordEventStart_43( + this, + Invocation.method(#pigeon_copy, []), + ), + ) + as _i2.VideoRecordEventStart); + + @override + _i5.Future<_i2.RecordingStats> getRecordingStats() => + (super.noSuchMethod( + Invocation.method(#getRecordingStats, []), + returnValue: _i5.Future<_i2.RecordingStats>.value( + _FakeRecordingStats_38(this, Invocation.method(#getRecordingStats, [])), + ), + returnValueForMissingStub: _i5.Future<_i2.RecordingStats>.value( + _FakeRecordingStats_38(this, Invocation.method(#getRecordingStats, [])), + ), + ) + as _i5.Future<_i2.RecordingStats>); +} + +/// A class which mocks [VideoRecordEventStatus]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockVideoRecordEventStatus extends _i1.Mock implements _i2.VideoRecordEventStatus { + @override + _i2.PigeonInstanceManager get pigeon_instanceManager => + (super.noSuchMethod( + Invocation.getter(#pigeon_instanceManager), + returnValue: _FakePigeonInstanceManager_0( + this, + Invocation.getter(#pigeon_instanceManager), + ), + returnValueForMissingStub: _FakePigeonInstanceManager_0( + this, + Invocation.getter(#pigeon_instanceManager), + ), + ) + as _i2.PigeonInstanceManager); + + @override + _i2.VideoRecordEventStatus pigeon_copy() => + (super.noSuchMethod( + Invocation.method(#pigeon_copy, []), + returnValue: _FakeVideoRecordEventStatus_44(this, Invocation.method(#pigeon_copy, [])), + returnValueForMissingStub: _FakeVideoRecordEventStatus_44( + this, + Invocation.method(#pigeon_copy, []), + ), + ) + as _i2.VideoRecordEventStatus); + + @override + _i5.Future<_i2.RecordingStats> getRecordingStats() => + (super.noSuchMethod( + Invocation.method(#getRecordingStats, []), + returnValue: _i5.Future<_i2.RecordingStats>.value( + _FakeRecordingStats_38(this, Invocation.method(#getRecordingStats, [])), + ), + returnValueForMissingStub: _i5.Future<_i2.RecordingStats>.value( + _FakeRecordingStats_38(this, Invocation.method(#getRecordingStats, [])), + ), + ) + as _i5.Future<_i2.RecordingStats>); +} + /// A class which mocks [ZoomState]. /// /// See the documentation for Mockito's code generation for more information. @@ -2167,8 +2390,8 @@ class MockZoomState extends _i1.Mock implements _i2.ZoomState { _i2.ZoomState pigeon_copy() => (super.noSuchMethod( Invocation.method(#pigeon_copy, []), - returnValue: _FakeZoomState_41(this, Invocation.method(#pigeon_copy, [])), - returnValueForMissingStub: _FakeZoomState_41(this, Invocation.method(#pigeon_copy, [])), + returnValue: _FakeZoomState_45(this, Invocation.method(#pigeon_copy, [])), + returnValueForMissingStub: _FakeZoomState_45(this, Invocation.method(#pigeon_copy, [])), ) as _i2.ZoomState); }