diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md
index 17c007087abf..cfc8400a712f 100644
--- a/packages/camera/camera_android_camerax/CHANGELOG.md
+++ b/packages/camera/camera_android_camerax/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 0.6.5
+
+* Modifies `stopVideoRecording` to ensure that the method only returns when CameraX reports that the
+ recorded video finishes saving to a file.
+* Modifies `startVideoCapturing` to ensure that the method only returns when CameraX reports that
+ video recording has started.
+* Adds empty implementation for `setDescriptionWhileRecording` and leaves a todo to add this feature.
+
## 0.6.4+1
* Adds empty implementation for `prepareForVideoRecording` since this optimization is not used on Android.
diff --git a/packages/camera/camera_android_camerax/README.md b/packages/camera/camera_android_camerax/README.md
index d7e31e92e03d..d68537ef1607 100644
--- a/packages/camera/camera_android_camerax/README.md
+++ b/packages/camera/camera_android_camerax/README.md
@@ -37,6 +37,10 @@ use cases, the plugin behaves according to the following:
video recording and image streaming is supported, but concurrent video recording, image
streaming, and image capture is not supported.
+### `setDescriptionWhileRecording` is unimplemented [Issue #148013][148013]
+`setDescriptionWhileRecording`, used to switch cameras while recording video, is currently unimplemented
+due to this not currently being supported by CameraX.
+
### 240p resolution configuration for video recording
240p resolution configuration for video recording is unsupported by CameraX,
@@ -64,11 +68,4 @@ For more information on contributing to this plugin, see [`CONTRIBUTING.md`](CON
[6]: https://developer.android.com/media/camera/camerax/architecture#combine-use-cases
[7]: https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#INFO_SUPPORTED_HARDWARE_LEVEL_3
[8]: https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
-[120462]: https://github.com/flutter/flutter/issues/120462
-[125915]: https://github.com/flutter/flutter/issues/125915
-[120715]: https://github.com/flutter/flutter/issues/120715
-[120468]: https://github.com/flutter/flutter/issues/120468
-[120467]: https://github.com/flutter/flutter/issues/120467
-[125371]: https://github.com/flutter/flutter/issues/125371
-[126477]: https://github.com/flutter/flutter/issues/126477
-[127896]: https://github.com/flutter/flutter/issues/127896
+[148013]: https://github.com/flutter/flutter/issues/148013
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 41094fd858aa..cbfc36f35cbd 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
@@ -146,6 +146,22 @@ private VideoResolutionFallbackRule(final int index) {
}
}
+ /**
+ * Video recording status.
+ *
+ *
See https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent.
+ */
+ public enum VideoRecordEvent {
+ START(0),
+ FINALIZE(1);
+
+ final int index;
+
+ private VideoRecordEvent(final int index) {
+ this.index = index;
+ }
+ }
+
/**
* The types of capture request options this plugin currently supports.
*
@@ -558,6 +574,55 @@ ArrayList toList() {
}
}
+ /** Generated class from Pigeon that represents data sent in messages. */
+ public static final class VideoRecordEventData {
+ private @NonNull VideoRecordEvent value;
+
+ public @NonNull VideoRecordEvent getValue() {
+ return value;
+ }
+
+ public void setValue(@NonNull VideoRecordEvent setterArg) {
+ if (setterArg == null) {
+ throw new IllegalStateException("Nonnull field \"value\" is null.");
+ }
+ this.value = setterArg;
+ }
+
+ /** Constructor is non-public to enforce null safety; use Builder. */
+ VideoRecordEventData() {}
+
+ public static final class Builder {
+
+ private @Nullable VideoRecordEvent value;
+
+ public @NonNull Builder setValue(@NonNull VideoRecordEvent setterArg) {
+ this.value = setterArg;
+ return this;
+ }
+
+ public @NonNull VideoRecordEventData build() {
+ VideoRecordEventData pigeonReturn = new VideoRecordEventData();
+ pigeonReturn.setValue(value);
+ return pigeonReturn;
+ }
+ }
+
+ @NonNull
+ ArrayList toList() {
+ ArrayList toListResult = new ArrayList(1);
+ toListResult.add(value == null ? null : value.index);
+ return toListResult;
+ }
+
+ static @NonNull VideoRecordEventData fromList(@NonNull ArrayList list) {
+ VideoRecordEventData pigeonResult = new VideoRecordEventData();
+ Object value = list.get(0);
+ pigeonResult.setValue(value == null ? null : VideoRecordEvent.values()[(int) value]);
+ return pigeonResult;
+ }
+ }
+
/**
* Convenience class for building [FocusMeteringAction]s with multiple metering points.
*
@@ -2118,6 +2183,34 @@ static void setup(
}
}
}
+
+ private static class PendingRecordingFlutterApiCodec extends StandardMessageCodec {
+ public static final PendingRecordingFlutterApiCodec INSTANCE =
+ new PendingRecordingFlutterApiCodec();
+
+ private PendingRecordingFlutterApiCodec() {}
+
+ @Override
+ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) {
+ switch (type) {
+ case (byte) 128:
+ return VideoRecordEventData.fromList((ArrayList) readValue(buffer));
+ default:
+ return super.readValueOfType(type, buffer);
+ }
+ }
+
+ @Override
+ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) {
+ if (value instanceof VideoRecordEventData) {
+ stream.write(128);
+ writeValue(stream, ((VideoRecordEventData) value).toList());
+ } else {
+ super.writeValue(stream, value);
+ }
+ }
+ }
+
/** Generated class from Pigeon that represents Flutter messages that can be called from Java. */
public static class PendingRecordingFlutterApi {
private final @NonNull BinaryMessenger binaryMessenger;
@@ -2133,7 +2226,7 @@ public interface Reply {
}
/** The codec used by PendingRecordingFlutterApi. */
static @NonNull MessageCodec getCodec() {
- return new StandardMessageCodec();
+ return PendingRecordingFlutterApiCodec.INSTANCE;
}
public void create(@NonNull Long identifierArg, @NonNull Reply callback) {
@@ -2144,6 +2237,18 @@ public void create(@NonNull Long identifierArg, @NonNull Reply callback) {
new ArrayList(Collections.singletonList(identifierArg)),
channelReply -> callback.reply(null));
}
+
+ public void onVideoRecordingEvent(
+ @NonNull VideoRecordEventData eventArg, @NonNull Reply callback) {
+ BasicMessageChannel channel =
+ new BasicMessageChannel<>(
+ binaryMessenger,
+ "dev.flutter.pigeon.PendingRecordingFlutterApi.onVideoRecordingEvent",
+ getCodec());
+ channel.send(
+ new ArrayList(Collections.singletonList(eventArg)),
+ channelReply -> callback.reply(null));
+ }
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
public interface RecordingHostApi {
@@ -4027,6 +4132,8 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) {
return ResolutionInfo.fromList((ArrayList) readValue(buffer));
case (byte) 134:
return VideoQualityData.fromList((ArrayList) readValue(buffer));
+ case (byte) 135:
+ return VideoRecordEventData.fromList((ArrayList) readValue(buffer));
default:
return super.readValueOfType(type, buffer);
}
@@ -4055,6 +4162,9 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) {
} else if (value instanceof VideoQualityData) {
stream.write(134);
writeValue(stream, ((VideoQualityData) value).toList());
+ } else if (value instanceof VideoRecordEventData) {
+ stream.write(135);
+ writeValue(stream, ((VideoRecordEventData) value).toList());
} else {
super.writeValue(stream, value);
}
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
index 9b4f71080562..b3c46769ad98 100644
--- 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
@@ -9,6 +9,8 @@
import androidx.camera.video.PendingRecording;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.PendingRecordingFlutterApi;
+import io.flutter.plugins.camerax.GeneratedCameraXLibrary.VideoRecordEvent;
+import io.flutter.plugins.camerax.GeneratedCameraXLibrary.VideoRecordEventData;
public class PendingRecordingFlutterApiImpl extends PendingRecordingFlutterApi {
private final InstanceManager instanceManager;
@@ -22,4 +24,14 @@ public PendingRecordingFlutterApiImpl(
void create(@NonNull PendingRecording pendingRecording, @Nullable Reply reply) {
create(instanceManager.addHostCreatedInstance(pendingRecording), reply);
}
+
+ void sendVideoRecordingFinalizedEvent(@NonNull Reply reply) {
+ super.onVideoRecordingEvent(
+ new VideoRecordEventData.Builder().setValue(VideoRecordEvent.FINALIZE).build(), reply);
+ }
+
+ void sendVideoRecordingStartedEvent(@NonNull Reply reply) {
+ super.onVideoRecordingEvent(
+ new VideoRecordEventData.Builder().setValue(VideoRecordEvent.START).build(), 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
index a1d661d1d9c1..93aa39c56bee 100644
--- 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
@@ -24,6 +24,8 @@ public class PendingRecordingHostApiImpl implements PendingRecordingHostApi {
@VisibleForTesting @NonNull public CameraXProxy cameraXProxy = new CameraXProxy();
+ @VisibleForTesting PendingRecordingFlutterApiImpl pendingRecordingFlutterApi;
+
@VisibleForTesting SystemServicesFlutterApiImpl systemServicesFlutterApi;
@VisibleForTesting RecordingFlutterApiImpl recordingFlutterApi;
@@ -37,6 +39,8 @@ public PendingRecordingHostApiImpl(
this.context = context;
systemServicesFlutterApi = cameraXProxy.createSystemServicesFlutterApiImpl(binaryMessenger);
recordingFlutterApi = new RecordingFlutterApiImpl(binaryMessenger, instanceManager);
+ pendingRecordingFlutterApi =
+ new PendingRecordingFlutterApiImpl(binaryMessenger, instanceManager);
}
/** Sets the context, which is used to get the {@link Executor} needed to start the recording. */
@@ -73,10 +77,16 @@ public Executor getExecutor() {
/**
* Handles {@link VideoRecordEvent}s that come in during video recording. Sends any errors
* encountered using {@link SystemServicesFlutterApiImpl}.
+ *
+ * Currently only sends {@link VideoRecordEvent.Start} and {@link VideoRecordEvent.Finalize}
+ * events to the Dart side.
*/
@VisibleForTesting
public void handleVideoRecordEvent(@NonNull VideoRecordEvent event) {
- if (event instanceof VideoRecordEvent.Finalize) {
+ if (event instanceof VideoRecordEvent.Start) {
+ pendingRecordingFlutterApi.sendVideoRecordingStartedEvent(reply -> {});
+ } else if (event instanceof VideoRecordEvent.Finalize) {
+ pendingRecordingFlutterApi.sendVideoRecordingFinalizedEvent(reply -> {});
VideoRecordEvent.Finalize castedEvent = (VideoRecordEvent.Finalize) event;
if (castedEvent.hasError()) {
String cameraErrorMessage;
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
index 92415d5381a0..f25a17ed5d91 100644
--- 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
@@ -41,6 +41,7 @@ public class PendingRecordingTest {
@Mock public RecordingFlutterApiImpl mockRecordingFlutterApi;
@Mock public Context mockContext;
@Mock public SystemServicesFlutterApiImpl mockSystemServicesFlutterApi;
+ @Mock public PendingRecordingFlutterApiImpl mockPendingRecordingFlutterApi;
@Mock public VideoRecordEvent.Finalize event;
@Mock public Throwable throwable;
@@ -80,6 +81,7 @@ public void testHandleVideoRecordEventSendsError() {
PendingRecordingHostApiImpl pendingRecordingHostApi =
new PendingRecordingHostApiImpl(mockBinaryMessenger, testInstanceManager, mockContext);
pendingRecordingHostApi.systemServicesFlutterApi = mockSystemServicesFlutterApi;
+ pendingRecordingHostApi.pendingRecordingFlutterApi = mockPendingRecordingFlutterApi;
final String eventMessage = "example failure message";
when(event.hasError()).thenReturn(true);
@@ -89,9 +91,35 @@ public void testHandleVideoRecordEventSendsError() {
pendingRecordingHostApi.handleVideoRecordEvent(event);
+ verify(mockPendingRecordingFlutterApi).sendVideoRecordingFinalizedEvent(any());
verify(mockSystemServicesFlutterApi).sendCameraError(eq(eventMessage), any());
}
+ @Test
+ public void handleVideoRecordEvent_SendsVideoRecordingFinalizedEvent() {
+ PendingRecordingHostApiImpl pendingRecordingHostApi =
+ new PendingRecordingHostApiImpl(mockBinaryMessenger, testInstanceManager, mockContext);
+ pendingRecordingHostApi.pendingRecordingFlutterApi = mockPendingRecordingFlutterApi;
+
+ when(event.hasError()).thenReturn(false);
+
+ pendingRecordingHostApi.handleVideoRecordEvent(event);
+
+ verify(mockPendingRecordingFlutterApi).sendVideoRecordingFinalizedEvent(any());
+ }
+
+ @Test
+ public void handleVideoRecordEvent_SendsVideoRecordingStartedEvent() {
+ PendingRecordingHostApiImpl pendingRecordingHostApi =
+ new PendingRecordingHostApiImpl(mockBinaryMessenger, testInstanceManager, mockContext);
+ pendingRecordingHostApi.pendingRecordingFlutterApi = mockPendingRecordingFlutterApi;
+ VideoRecordEvent.Start mockStartEvent = mock(VideoRecordEvent.Start.class);
+
+ pendingRecordingHostApi.handleVideoRecordEvent(mockStartEvent);
+
+ verify(mockPendingRecordingFlutterApi).sendVideoRecordingStartedEvent(any());
+ }
+
@Test
public void flutterApiCreateTest() {
final PendingRecordingFlutterApiImpl spyPendingRecordingFlutterApi =
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 83d20b3585b4..915f522d239e 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
@@ -13,6 +13,7 @@ import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
+import 'package:video_player/video_player.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -178,4 +179,81 @@ void main() {
}
}
});
+
+ testWidgets('Video capture records valid video', (WidgetTester tester) async {
+ final List cameras = await availableCameras();
+ if (cameras.isEmpty) {
+ return;
+ }
+
+ final CameraController controller = CameraController(cameras[0],
+ mediaSettings:
+ const MediaSettings(resolutionPreset: ResolutionPreset.low));
+ await controller.initialize();
+ await controller.prepareForVideoRecording();
+
+ await controller.startVideoRecording();
+ final int recordingStart = DateTime.now().millisecondsSinceEpoch;
+
+ sleep(const Duration(seconds: 2));
+
+ final XFile file = await controller.stopVideoRecording();
+ final int postStopTime =
+ DateTime.now().millisecondsSinceEpoch - recordingStart;
+
+ final File videoFile = File(file.path);
+ final VideoPlayerController videoController = VideoPlayerController.file(
+ videoFile,
+ );
+ await videoController.initialize();
+ final int duration = videoController.value.duration.inMilliseconds;
+ await videoController.dispose();
+
+ expect(duration, lessThan(postStopTime));
+ });
+
+ testWidgets('Pause and resume video recording', (WidgetTester tester) async {
+ final List cameras = await availableCameras();
+ if (cameras.isEmpty) {
+ return;
+ }
+
+ final CameraController controller = CameraController(cameras[0],
+ mediaSettings:
+ const MediaSettings(resolutionPreset: ResolutionPreset.low));
+ await controller.initialize();
+ await controller.prepareForVideoRecording();
+
+ int startPause;
+ int timePaused = 0;
+ const int pauseIterations = 2;
+
+ await controller.startVideoRecording();
+ final int recordingStart = DateTime.now().millisecondsSinceEpoch;
+ sleep(const Duration(milliseconds: 500));
+
+ for (int i = 0; i < pauseIterations; i++) {
+ await controller.pauseVideoRecording();
+ startPause = DateTime.now().millisecondsSinceEpoch;
+ sleep(const Duration(milliseconds: 500));
+ await controller.resumeVideoRecording();
+ timePaused += DateTime.now().millisecondsSinceEpoch - startPause;
+
+ sleep(const Duration(milliseconds: 500));
+ }
+
+ final XFile file = await controller.stopVideoRecording();
+ final int recordingTime =
+ DateTime.now().millisecondsSinceEpoch - recordingStart;
+
+ final File videoFile = File(file.path);
+ final VideoPlayerController videoController = VideoPlayerController.file(
+ videoFile,
+ );
+ await videoController.initialize();
+ final int duration = videoController.value.duration.inMilliseconds;
+ await videoController.dispose();
+
+ expect(duration, lessThan(recordingTime - timePaused));
+ });
}
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 5ced7ccccb38..edf24bab37a0 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
@@ -113,6 +113,12 @@ class AndroidCameraCameraX extends CameraPlatform {
@visibleForTesting
String? videoOutputPath;
+ /// Stream queue to pick up finalized viceo recording events in
+ /// [stopVideoRecording].
+ final StreamQueue videoRecordingEventStreamQueue =
+ StreamQueue(
+ PendingRecording.videoRecordingEventStreamController.stream);
+
/// Whether or not [preview] has been bound to the lifecycle of the camera by
/// [createCamera].
@visibleForTesting
@@ -122,7 +128,7 @@ class AndroidCameraCameraX extends CameraPlatform {
/// The prefix used to create the filename for video recording files.
@visibleForTesting
- final String videoPrefix = 'MOV';
+ final String videoPrefix = 'REC';
/// The [ImageCapture] instance that can be configured to capture a still image.
@visibleForTesting
@@ -777,6 +783,15 @@ class AndroidCameraCameraX extends CameraPlatform {
await _unbindUseCaseFromLifecycle(preview!);
}
+ /// Sets the active camera while recording.
+ ///
+ /// Currently unsupported, so is a no-op.
+ @override
+ Future setDescriptionWhileRecording(CameraDescription description) {
+ // TODO(camsim99): Implement this feature, see https://github.com/flutter/flutter/issues/148013.
+ return Future.value();
+ }
+
/// Resume the paused preview for the selected camera.
///
/// [cameraId] not used.
@@ -963,6 +978,12 @@ class AndroidCameraCameraX extends CameraPlatform {
if (streamCallback != null) {
onStreamedFrameAvailable(options.cameraId).listen(streamCallback);
}
+
+ // Wait for video recording to start.
+ VideoRecordEvent event = await videoRecordingEventStreamQueue.next;
+ while (event != VideoRecordEvent.start) {
+ event = await videoRecordingEventStreamQueue.next;
+ }
}
/// Stops the video recording and returns the file where it was saved.
@@ -979,23 +1000,30 @@ class AndroidCameraCameraX extends CameraPlatform {
'Attempting to stop a '
'video recording while no recording is in progress.');
}
+
+ /// Stop the active recording and wait for the video recording to be finalized.
+ await recording!.close();
+ VideoRecordEvent event = await videoRecordingEventStreamQueue.next;
+ while (event != VideoRecordEvent.finalize) {
+ event = await videoRecordingEventStreamQueue.next;
+ }
+ recording = null;
+ pendingRecording = null;
+
if (videoOutputPath == null) {
- // Stop the current active recording as we will be unable to complete it
- // in this error case.
- await recording!.close();
- recording = null;
- pendingRecording = null;
+ // Handle any errors with finalizing video recording.
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.');
}
- await recording!.close();
- recording = null;
- pendingRecording = null;
+
await _unbindUseCaseFromLifecycle(videoCapture!);
- return XFile(videoOutputPath!);
+ final XFile videoFile = XFile(videoOutputPath!);
+ cameraEventStreamController
+ .add(VideoRecordedEvent(cameraId, videoFile, /* duration */ null));
+ return videoFile;
}
/// Pause the current video recording if it is not null.
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 550854fba3e1..e63a7a6afaf9 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
@@ -68,6 +68,14 @@ enum VideoResolutionFallbackRule {
lowerQualityThan,
}
+/// Video recording status.
+///
+/// See https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent.
+enum VideoRecordEvent {
+ start,
+ finalize,
+}
+
/// The types of capture request options this plugin currently supports.
///
/// If you need to add another option to support, ensure the following is done
@@ -232,6 +240,27 @@ class VideoQualityData {
}
}
+class VideoRecordEventData {
+ VideoRecordEventData({
+ required this.value,
+ });
+
+ VideoRecordEvent value;
+
+ Object encode() {
+ return [
+ value.index,
+ ];
+ }
+
+ static VideoRecordEventData decode(Object result) {
+ result as List;
+ return VideoRecordEventData(
+ value: VideoRecordEvent.values[result[0]! as int],
+ );
+ }
+}
+
/// Convenience class for building [FocusMeteringAction]s with multiple metering
/// points.
class MeteringPointInfo {
@@ -1580,11 +1609,36 @@ class PendingRecordingHostApi {
}
}
+class _PendingRecordingFlutterApiCodec extends StandardMessageCodec {
+ const _PendingRecordingFlutterApiCodec();
+ @override
+ void writeValue(WriteBuffer buffer, Object? value) {
+ if (value is VideoRecordEventData) {
+ buffer.putUint8(128);
+ writeValue(buffer, value.encode());
+ } else {
+ super.writeValue(buffer, value);
+ }
+ }
+
+ @override
+ Object? readValueOfType(int type, ReadBuffer buffer) {
+ switch (type) {
+ case 128:
+ return VideoRecordEventData.decode(readValue(buffer)!);
+ default:
+ return super.readValueOfType(type, buffer);
+ }
+ }
+}
+
abstract class PendingRecordingFlutterApi {
- static const MessageCodec codec = StandardMessageCodec();
+ static const MessageCodec codec = _PendingRecordingFlutterApiCodec();
void create(int identifier);
+ void onVideoRecordingEvent(VideoRecordEventData event);
+
static void setup(PendingRecordingFlutterApi? api,
{BinaryMessenger? binaryMessenger}) {
{
@@ -1606,6 +1660,27 @@ abstract class PendingRecordingFlutterApi {
});
}
}
+ {
+ final BasicMessageChannel channel = BasicMessageChannel(
+ 'dev.flutter.pigeon.PendingRecordingFlutterApi.onVideoRecordingEvent',
+ codec,
+ binaryMessenger: binaryMessenger);
+ if (api == null) {
+ channel.setMessageHandler(null);
+ } else {
+ channel.setMessageHandler((Object? message) async {
+ assert(message != null,
+ 'Argument for dev.flutter.pigeon.PendingRecordingFlutterApi.onVideoRecordingEvent was null.');
+ final List args = (message as List?)!;
+ final VideoRecordEventData? arg_event =
+ (args[0] as VideoRecordEventData?);
+ assert(arg_event != null,
+ 'Argument for dev.flutter.pigeon.PendingRecordingFlutterApi.onVideoRecordingEvent was null, expected non-null VideoRecordEventData.');
+ api.onVideoRecordingEvent(arg_event!);
+ return;
+ });
+ }
+ }
}
}
@@ -3222,6 +3297,9 @@ class _CaptureRequestOptionsHostApiCodec extends StandardMessageCodec {
} else if (value is VideoQualityData) {
buffer.putUint8(134);
writeValue(buffer, value.encode());
+ } else if (value is VideoRecordEventData) {
+ buffer.putUint8(135);
+ writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
@@ -3244,6 +3322,8 @@ class _CaptureRequestOptionsHostApiCodec extends StandardMessageCodec {
return ResolutionInfo.decode(readValue(buffer)!);
case 134:
return VideoQualityData.decode(readValue(buffer)!);
+ case 135:
+ return VideoRecordEventData.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
}
diff --git a/packages/camera/camera_android_camerax/lib/src/pending_recording.dart b/packages/camera/camera_android_camerax/lib/src/pending_recording.dart
index 971ef49390ac..7dcb19e48c5d 100644
--- a/packages/camera/camera_android_camerax/lib/src/pending_recording.dart
+++ b/packages/camera/camera_android_camerax/lib/src/pending_recording.dart
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'dart:async';
+
import 'package:flutter/services.dart' show BinaryMessenger;
import 'package:meta/meta.dart' show immutable;
@@ -30,6 +32,11 @@ class PendingRecording extends JavaObject {
late final PendingRecordingHostApiImpl _api;
+ /// Stream that emits an event when the corresponding video recording is finalized.
+ static final StreamController
+ videoRecordingEventStreamController =
+ StreamController.broadcast();
+
/// Starts the recording, making it an active recording.
Future start() {
return _api.startFromInstance(this);
@@ -100,4 +107,9 @@ class PendingRecordingFlutterApiImpl extends PendingRecordingFlutterApi {
);
});
}
+
+ @override
+ void onVideoRecordingEvent(VideoRecordEventData event) {
+ PendingRecording.videoRecordingEventStreamController.add(event.value);
+ }
}
diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart
index f51eae8c306a..872c3a622390 100644
--- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart
+++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart
@@ -126,6 +126,15 @@ enum VideoResolutionFallbackRule {
lowerQualityThan,
}
+/// Video recording status.
+///
+/// See https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent.
+enum VideoRecordEvent { start, finalize }
+
+class VideoRecordEventData {
+ late VideoRecordEvent value;
+}
+
/// Convenience class for building [FocusMeteringAction]s with multiple metering
/// points.
class MeteringPointInfo {
@@ -325,6 +334,8 @@ abstract class PendingRecordingHostApi {
@FlutterApi()
abstract class PendingRecordingFlutterApi {
void create(int identifier);
+
+ void onVideoRecordingEvent(VideoRecordEventData event);
}
@HostApi(dartHostTestHandler: 'TestRecordingHostApi')
diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml
index cabd26b66a2d..9d4191b98b2e 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.6.4+1
+version: 0.6.5
environment:
sdk: ^3.1.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 23af67c5ca18..aa33254ac14a 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
@@ -1290,7 +1290,7 @@ void main() {
Future.value(mockCamera2CameraInfo));
const int cameraId = 17;
- const String outputPath = '/temp/MOV123.temp';
+ const String outputPath = '/temp/REC123.temp';
// Mock method calls.
when(mockSystemServicesApi.getTempFilePath(camera.videoPrefix, '.temp'))
@@ -1312,6 +1312,10 @@ void main() {
when(mockCamera2CameraInfo.getSupportedHardwareLevel()).thenAnswer(
(_) async => CameraMetadata.infoSupportedHardwareLevelLimited);
+ // Simulate video recording being started so startVideoRecording completes.
+ PendingRecording.videoRecordingEventStreamController
+ .add(VideoRecordEvent.start);
+
await camera.startVideoCapturing(const VideoCaptureOptions(cameraId));
// Verify VideoCapture UseCase is bound and camera & its properties
@@ -1369,7 +1373,7 @@ void main() {
Future.value(mockCamera2CameraInfo));
const int cameraId = 17;
- const String outputPath = '/temp/MOV123.temp';
+ const String outputPath = '/temp/REC123.temp';
// Mock method calls.
when(mockSystemServicesApi.getTempFilePath(camera.videoPrefix, '.temp'))
@@ -1389,6 +1393,10 @@ void main() {
when(mockCamera2CameraInfo.getSupportedHardwareLevel()).thenAnswer(
(_) async => CameraMetadata.infoSupportedHardwareLevelLimited);
+ // Simulate video recording being started so startVideoRecording completes.
+ PendingRecording.videoRecordingEventStreamController
+ .add(VideoRecordEvent.start);
+
await camera.startVideoCapturing(const VideoCaptureOptions(cameraId));
verify(camera.processCameraProvider!.bindToLifecycle(
@@ -1448,7 +1456,7 @@ void main() {
: MockCamera2CameraInfo());
const int cameraId = 17;
- const String outputPath = '/temp/MOV123.temp';
+ const String outputPath = '/temp/REC123.temp';
final Completer imageDataCompleter =
Completer();
final VideoCaptureOptions videoCaptureOptions = VideoCaptureOptions(
@@ -1472,6 +1480,10 @@ void main() {
when(mockCamera2CameraInfo.getSupportedHardwareLevel())
.thenAnswer((_) async => CameraMetadata.infoSupportedHardwareLevel3);
+ // Simulate video recording being started so startVideoRecording completes.
+ PendingRecording.videoRecordingEventStreamController
+ .add(VideoRecordEvent.start);
+
await camera.startVideoCapturing(videoCaptureOptions);
final CameraImageData mockCameraImageData = MockCameraImageData();
@@ -1516,7 +1528,7 @@ void main() {
: MockCamera2CameraInfo());
const int cameraId = 87;
- const String outputPath = '/temp/MOV123.temp';
+ const String outputPath = '/temp/REC123.temp';
// Mock method calls.
when(mockSystemServicesApi.getTempFilePath(camera.videoPrefix, '.temp'))
@@ -1529,12 +1541,20 @@ void main() {
when(camera.processCameraProvider!.isBound(camera.imageAnalysis!))
.thenAnswer((_) async => false);
+ // Simulate video recording being started so startVideoRecording completes.
+ PendingRecording.videoRecordingEventStreamController
+ .add(VideoRecordEvent.start);
+
// Orientation is unlocked and plugin does not need to set default target
// rotation manually.
camera.recording = null;
await camera.startVideoCapturing(const VideoCaptureOptions(cameraId));
verifyNever(mockVideoCapture.setTargetRotation(any));
+ // Simulate video recording being started so startVideoRecording completes.
+ PendingRecording.videoRecordingEventStreamController
+ .add(VideoRecordEvent.start);
+
// Orientation is locked and plugin does not need to set default target
// rotation manually.
camera.recording = null;
@@ -1542,6 +1562,10 @@ void main() {
await camera.startVideoCapturing(const VideoCaptureOptions(cameraId));
verifyNever(mockVideoCapture.setTargetRotation(any));
+ // Simulate video recording being started so startVideoRecording completes.
+ PendingRecording.videoRecordingEventStreamController
+ .add(VideoRecordEvent.start);
+
// Orientation is locked and plugin does need to set default target
// rotation manually.
camera.recording = null;
@@ -1550,6 +1574,10 @@ void main() {
await camera.startVideoCapturing(const VideoCaptureOptions(cameraId));
verifyNever(mockVideoCapture.setTargetRotation(any));
+ // Simulate video recording being started so startVideoRecording completes.
+ PendingRecording.videoRecordingEventStreamController
+ .add(VideoRecordEvent.start);
+
// Orientation is unlocked and plugin does need to set default target
// rotation manually.
camera.recording = null;
@@ -1601,6 +1629,10 @@ void main() {
when(camera.processCameraProvider!.isBound(videoCapture))
.thenAnswer((_) async => true);
+ // Simulate video recording being finalized so stopVideoRecording completes.
+ PendingRecording.videoRecordingEventStreamController
+ .add(VideoRecordEvent.finalize);
+
final XFile file = await camera.stopVideoRecording(0);
expect(file.path, videoOutputPath);
@@ -1642,6 +1674,9 @@ void main() {
.thenAnswer((_) async => true);
await expectLater(() async {
+ // Simulate video recording being finalized so stopVideoRecording completes.
+ PendingRecording.videoRecordingEventStreamController
+ .add(VideoRecordEvent.finalize);
await camera.stopVideoRecording(0);
}, throwsA(isA()));
expect(camera.recording, null);
@@ -1663,6 +1698,10 @@ void main() {
camera.videoCapture = videoCapture;
camera.videoOutputPath = videoOutputPath;
+ // Simulate video recording being finalized so stopVideoRecording completes.
+ PendingRecording.videoRecordingEventStreamController
+ .add(VideoRecordEvent.finalize);
+
final XFile file = await camera.stopVideoRecording(0);
expect(file.path, videoOutputPath);
@@ -1691,6 +1730,10 @@ void main() {
when(camera.processCameraProvider!.isBound(videoCapture))
.thenAnswer((_) async => true);
+ // Simulate video recording being finalized so stopVideoRecording completes.
+ PendingRecording.videoRecordingEventStreamController
+ .add(VideoRecordEvent.finalize);
+
await camera.stopVideoRecording(90);
verify(processCameraProvider.unbind([videoCapture]));
@@ -1698,6 +1741,28 @@ void main() {
verify(recording.close());
verifyNoMoreInteractions(recording);
});
+
+ test(
+ 'setDescriptionWhileRecording does not make any calls involving starting video recording',
+ () async {
+ // TODO(camsim99): Modify test when implemented, see https://github.com/flutter/flutter/issues/148013.
+ final AndroidCameraCameraX camera = AndroidCameraCameraX();
+
+ // Set directly for test versus calling createCamera.
+ camera.processCameraProvider = MockProcessCameraProvider();
+ camera.recorder = MockRecorder();
+ camera.videoCapture = MockVideoCapture();
+ camera.camera = MockCamera();
+
+ await camera.setDescriptionWhileRecording(const CameraDescription(
+ name: 'fakeCameraName',
+ lensDirection: CameraLensDirection.back,
+ sensorOrientation: 90));
+ verifyNoMoreInteractions(camera.processCameraProvider);
+ verifyNoMoreInteractions(camera.recorder);
+ verifyNoMoreInteractions(camera.videoCapture);
+ verifyNoMoreInteractions(camera.camera);
+ });
});
test(
@@ -3695,7 +3760,7 @@ void main() {
Future.value(mockCamera2CameraInfo));
const int cameraId = 7;
- const String outputPath = '/temp/MOV123.temp';
+ const String outputPath = '/temp/REC123.temp';
// Mock method calls.
when(mockSystemServicesApi.getTempFilePath(camera.videoPrefix, '.temp'))
@@ -3717,6 +3782,10 @@ void main() {
when(mockCamera2CameraInfo.getSupportedHardwareLevel())
.thenAnswer((_) async => CameraMetadata.infoSupportedHardwareLevelFull);
+ // Simulate video recording being started so startVideoRecording completes.
+ PendingRecording.videoRecordingEventStreamController
+ .add(VideoRecordEvent.start);
+
await camera.startVideoCapturing(const VideoCaptureOptions(cameraId));
verify(
@@ -3756,7 +3825,7 @@ void main() {
Future.value(mockCamera2CameraInfo));
const int cameraId = 77;
- const String outputPath = '/temp/MOV123.temp';
+ const String outputPath = '/temp/REC123.temp';
// Mock method calls.
when(mockSystemServicesApi.getTempFilePath(camera.videoPrefix, '.temp'))
@@ -3778,6 +3847,10 @@ void main() {
when(mockCamera2CameraInfo.getSupportedHardwareLevel())
.thenAnswer((_) async => CameraMetadata.infoSupportedHardwareLevel3);
+ // Simulate video recording being started so startVideoRecording completes.
+ PendingRecording.videoRecordingEventStreamController
+ .add(VideoRecordEvent.start);
+
await camera.startVideoCapturing(const VideoCaptureOptions(cameraId));
verify(
@@ -3817,7 +3890,7 @@ void main() {
Future.value(mockCamera2CameraInfo));
const int cameraId = 87;
- const String outputPath = '/temp/MOV123.temp';
+ const String outputPath = '/temp/REC123.temp';
// Mock method calls.
when(mockSystemServicesApi.getTempFilePath(camera.videoPrefix, '.temp'))
@@ -3839,6 +3912,10 @@ void main() {
when(mockCamera2CameraInfo.getSupportedHardwareLevel()).thenAnswer(
(_) async => CameraMetadata.infoSupportedHardwareLevelExternal);
+ // Simulate video recording being started so startVideoRecording completes.
+ PendingRecording.videoRecordingEventStreamController
+ .add(VideoRecordEvent.start);
+
await camera.startVideoCapturing(VideoCaptureOptions(cameraId,
streamCallback: (CameraImageData image) {}));
verify(
@@ -3882,7 +3959,7 @@ void main() {
Future.value(mockCamera2CameraInfo));
const int cameraId = 107;
- const String outputPath = '/temp/MOV123.temp';
+ const String outputPath = '/temp/REC123.temp';
// Mock method calls.
when(mockSystemServicesApi.getTempFilePath(camera.videoPrefix, '.temp'))
@@ -3906,6 +3983,10 @@ void main() {
when(mockCamera2CameraInfo.getSupportedHardwareLevel())
.thenAnswer((_) async => CameraMetadata.infoSupportedHardwareLevel3);
+ // Simulate video recording being started so startVideoRecording completes.
+ PendingRecording.videoRecordingEventStreamController
+ .add(VideoRecordEvent.start);
+
await camera.startVideoCapturing(VideoCaptureOptions(cameraId,
streamCallback: (CameraImageData image) {}));
verify(
@@ -3947,7 +4028,7 @@ void main() {
Future.value(mockCamera2CameraInfo));
const int cameraId = 97;
- const String outputPath = '/temp/MOV123.temp';
+ const String outputPath = '/temp/REC123.temp';
// Mock method calls.
when(mockSystemServicesApi.getTempFilePath(camera.videoPrefix, '.temp'))
@@ -3966,6 +4047,11 @@ void main() {
.thenAnswer((_) async => MockLiveCameraState());
await camera.pausePreview(cameraId);
+
+ // Simulate video recording being started so startVideoRecording completes.
+ PendingRecording.videoRecordingEventStreamController
+ .add(VideoRecordEvent.start);
+
await camera.startVideoCapturing(const VideoCaptureOptions(cameraId));
verifyNever(
@@ -4009,7 +4095,7 @@ void main() {
Future.value(mockCamera2CameraInfo));
const int cameraId = 44;
- const String outputPath = '/temp/MOV123.temp';
+ const String outputPath = '/temp/REC123.temp';
// Mock method calls.
when(mockSystemServicesApi.getTempFilePath(camera.videoPrefix, '.temp'))
@@ -4033,6 +4119,10 @@ void main() {
when(mockCamera2CameraInfo.getSupportedHardwareLevel()).thenAnswer(
(_) async => CameraMetadata.infoSupportedHardwareLevelLegacy);
+ // Simulate video recording being started so startVideoRecording completes.
+ PendingRecording.videoRecordingEventStreamController
+ .add(VideoRecordEvent.start);
+
await camera.startVideoCapturing(const VideoCaptureOptions(cameraId));
verify(
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 8a659e460cd7..105447e52689 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
@@ -2230,6 +2230,9 @@ class _TestCaptureRequestOptionsHostApiCodec extends StandardMessageCodec {
} else if (value is VideoQualityData) {
buffer.putUint8(134);
writeValue(buffer, value.encode());
+ } else if (value is VideoRecordEventData) {
+ buffer.putUint8(135);
+ writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
@@ -2252,6 +2255,8 @@ class _TestCaptureRequestOptionsHostApiCodec extends StandardMessageCodec {
return ResolutionInfo.decode(readValue(buffer)!);
case 134:
return VideoQualityData.decode(readValue(buffer)!);
+ case 135:
+ return VideoRecordEventData.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
}