Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[camerax] Make fixes required to swap camera_android_camerax for camera_android #6697

Merged
merged 9 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/camera/camera_android_camerax/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
13 changes: 5 additions & 8 deletions packages/camera/camera_android_camerax/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
camsim99 marked this conversation as resolved.
Show resolved Hide resolved
[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
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,22 @@ private VideoResolutionFallbackRule(final int index) {
}
}

/**
* Video recording status.
*
* <p>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.
*
Expand Down Expand Up @@ -558,6 +574,55 @@ ArrayList<Object> 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<Object> toList() {
ArrayList<Object> toListResult = new ArrayList<Object>(1);
toListResult.add(value == null ? null : value.index);
return toListResult;
}

static @NonNull VideoRecordEventData fromList(@NonNull ArrayList<Object> 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.
*
Expand Down Expand Up @@ -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<Object>) 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;
Expand All @@ -2133,7 +2226,7 @@ public interface Reply<T> {
}
/** The codec used by PendingRecordingFlutterApi. */
static @NonNull MessageCodec<Object> getCodec() {
return new StandardMessageCodec();
return PendingRecordingFlutterApiCodec.INSTANCE;
}

public void create(@NonNull Long identifierArg, @NonNull Reply<Void> callback) {
Expand All @@ -2144,6 +2237,18 @@ public void create(@NonNull Long identifierArg, @NonNull Reply<Void> callback) {
new ArrayList<Object>(Collections.singletonList(identifierArg)),
channelReply -> callback.reply(null));
}

public void onVideoRecordingEvent(
@NonNull VideoRecordEventData eventArg, @NonNull Reply<Void> callback) {
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.PendingRecordingFlutterApi.onVideoRecordingEvent",
getCodec());
channel.send(
new ArrayList<Object>(Collections.singletonList(eventArg)),
channelReply -> callback.reply(null));
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
public interface RecordingHostApi {
Expand Down Expand Up @@ -4027,6 +4132,8 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) {
return ResolutionInfo.fromList((ArrayList<Object>) readValue(buffer));
case (byte) 134:
return VideoQualityData.fromList((ArrayList<Object>) readValue(buffer));
case (byte) 135:
return VideoRecordEventData.fromList((ArrayList<Object>) readValue(buffer));
default:
return super.readValueOfType(type, buffer);
}
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,4 +24,14 @@ public PendingRecordingFlutterApiImpl(
void create(@NonNull PendingRecording pendingRecording, @Nullable Reply<Void> reply) {
create(instanceManager.addHostCreatedInstance(pendingRecording), reply);
}

void sendVideoRecordingFinalizedEvent(@NonNull Reply<Void> reply) {
super.onVideoRecordingEvent(
new VideoRecordEventData.Builder().setValue(VideoRecordEvent.FINALIZE).build(), reply);
}

void sendVideoRecordingStartedEvent(@NonNull Reply<Void> reply) {
super.onVideoRecordingEvent(
new VideoRecordEventData.Builder().setValue(VideoRecordEvent.START).build(), reply);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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. */
Expand Down Expand Up @@ -73,10 +77,16 @@ public Executor getExecutor() {
/**
* Handles {@link VideoRecordEvent}s that come in during video recording. Sends any errors
* encountered using {@link SystemServicesFlutterApiImpl}.
*
* <p>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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand All @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -178,4 +179,81 @@ void main() {
}
}
});

testWidgets('Video capture records valid video', (WidgetTester tester) async {
final List<CameraDescription> 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<CameraDescription> 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));
});
}
Loading