Skip to content

Commit 1ab2f5b

Browse files
authored
[camerax] Make fixes required to swap camera_android_camerax for camera_android (#6697)
Makes changes needed to land #6629. Specifically: - Fixes timing issue with `stopVideoRecording` such that the `Future` it returns will only complete when CameraX reports that the recording is finalized (via listening for the [finalized video recording event](https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent.Finalize)) - Modifies `startVideoCapturing` such that the `Future` it returns will only complete when CameraX reports that video capturing has started (via listening for the [started video recording event](https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent.Start)) - Adds empty implementation and TODO for implementing `setDescriptionWhileRecording`
1 parent a8e9147 commit 1ab2f5b

File tree

14 files changed

+501
-32
lines changed

14 files changed

+501
-32
lines changed

packages/camera/camera_android_camerax/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## 0.6.5
2+
3+
* Modifies `stopVideoRecording` to ensure that the method only returns when CameraX reports that the
4+
recorded video finishes saving to a file.
5+
* Modifies `startVideoCapturing` to ensure that the method only returns when CameraX reports that
6+
video recording has started.
7+
* Adds empty implementation for `setDescriptionWhileRecording` and leaves a todo to add this feature.
8+
19
## 0.6.4+1
210

311
* Adds empty implementation for `prepareForVideoRecording` since this optimization is not used on Android.

packages/camera/camera_android_camerax/README.md

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ use cases, the plugin behaves according to the following:
3737
video recording and image streaming is supported, but concurrent video recording, image
3838
streaming, and image capture is not supported.
3939

40+
### `setDescriptionWhileRecording` is unimplemented [Issue #148013][148013]
41+
`setDescriptionWhileRecording`, used to switch cameras while recording video, is currently unimplemented
42+
due to this not currently being supported by CameraX.
43+
4044
### 240p resolution configuration for video recording
4145

4246
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
6468
[6]: https://developer.android.com/media/camera/camerax/architecture#combine-use-cases
6569
[7]: https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#INFO_SUPPORTED_HARDWARE_LEVEL_3
6670
[8]: https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
67-
[120462]: https://github.com/flutter/flutter/issues/120462
68-
[125915]: https://github.com/flutter/flutter/issues/125915
69-
[120715]: https://github.com/flutter/flutter/issues/120715
70-
[120468]: https://github.com/flutter/flutter/issues/120468
71-
[120467]: https://github.com/flutter/flutter/issues/120467
72-
[125371]: https://github.com/flutter/flutter/issues/125371
73-
[126477]: https://github.com/flutter/flutter/issues/126477
74-
[127896]: https://github.com/flutter/flutter/issues/127896
71+
[148013]: https://github.com/flutter/flutter/issues/148013

packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,22 @@ private VideoResolutionFallbackRule(final int index) {
146146
}
147147
}
148148

149+
/**
150+
* Video recording status.
151+
*
152+
* <p>See https://developer.android.com/reference/androidx/camera/video/VideoRecordEvent.
153+
*/
154+
public enum VideoRecordEvent {
155+
START(0),
156+
FINALIZE(1);
157+
158+
final int index;
159+
160+
private VideoRecordEvent(final int index) {
161+
this.index = index;
162+
}
163+
}
164+
149165
/**
150166
* The types of capture request options this plugin currently supports.
151167
*
@@ -558,6 +574,55 @@ ArrayList<Object> toList() {
558574
}
559575
}
560576

577+
/** Generated class from Pigeon that represents data sent in messages. */
578+
public static final class VideoRecordEventData {
579+
private @NonNull VideoRecordEvent value;
580+
581+
public @NonNull VideoRecordEvent getValue() {
582+
return value;
583+
}
584+
585+
public void setValue(@NonNull VideoRecordEvent setterArg) {
586+
if (setterArg == null) {
587+
throw new IllegalStateException("Nonnull field \"value\" is null.");
588+
}
589+
this.value = setterArg;
590+
}
591+
592+
/** Constructor is non-public to enforce null safety; use Builder. */
593+
VideoRecordEventData() {}
594+
595+
public static final class Builder {
596+
597+
private @Nullable VideoRecordEvent value;
598+
599+
public @NonNull Builder setValue(@NonNull VideoRecordEvent setterArg) {
600+
this.value = setterArg;
601+
return this;
602+
}
603+
604+
public @NonNull VideoRecordEventData build() {
605+
VideoRecordEventData pigeonReturn = new VideoRecordEventData();
606+
pigeonReturn.setValue(value);
607+
return pigeonReturn;
608+
}
609+
}
610+
611+
@NonNull
612+
ArrayList<Object> toList() {
613+
ArrayList<Object> toListResult = new ArrayList<Object>(1);
614+
toListResult.add(value == null ? null : value.index);
615+
return toListResult;
616+
}
617+
618+
static @NonNull VideoRecordEventData fromList(@NonNull ArrayList<Object> list) {
619+
VideoRecordEventData pigeonResult = new VideoRecordEventData();
620+
Object value = list.get(0);
621+
pigeonResult.setValue(value == null ? null : VideoRecordEvent.values()[(int) value]);
622+
return pigeonResult;
623+
}
624+
}
625+
561626
/**
562627
* Convenience class for building [FocusMeteringAction]s with multiple metering points.
563628
*
@@ -2118,6 +2183,34 @@ static void setup(
21182183
}
21192184
}
21202185
}
2186+
2187+
private static class PendingRecordingFlutterApiCodec extends StandardMessageCodec {
2188+
public static final PendingRecordingFlutterApiCodec INSTANCE =
2189+
new PendingRecordingFlutterApiCodec();
2190+
2191+
private PendingRecordingFlutterApiCodec() {}
2192+
2193+
@Override
2194+
protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) {
2195+
switch (type) {
2196+
case (byte) 128:
2197+
return VideoRecordEventData.fromList((ArrayList<Object>) readValue(buffer));
2198+
default:
2199+
return super.readValueOfType(type, buffer);
2200+
}
2201+
}
2202+
2203+
@Override
2204+
protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) {
2205+
if (value instanceof VideoRecordEventData) {
2206+
stream.write(128);
2207+
writeValue(stream, ((VideoRecordEventData) value).toList());
2208+
} else {
2209+
super.writeValue(stream, value);
2210+
}
2211+
}
2212+
}
2213+
21212214
/** Generated class from Pigeon that represents Flutter messages that can be called from Java. */
21222215
public static class PendingRecordingFlutterApi {
21232216
private final @NonNull BinaryMessenger binaryMessenger;
@@ -2133,7 +2226,7 @@ public interface Reply<T> {
21332226
}
21342227
/** The codec used by PendingRecordingFlutterApi. */
21352228
static @NonNull MessageCodec<Object> getCodec() {
2136-
return new StandardMessageCodec();
2229+
return PendingRecordingFlutterApiCodec.INSTANCE;
21372230
}
21382231

21392232
public void create(@NonNull Long identifierArg, @NonNull Reply<Void> callback) {
@@ -2144,6 +2237,18 @@ public void create(@NonNull Long identifierArg, @NonNull Reply<Void> callback) {
21442237
new ArrayList<Object>(Collections.singletonList(identifierArg)),
21452238
channelReply -> callback.reply(null));
21462239
}
2240+
2241+
public void onVideoRecordingEvent(
2242+
@NonNull VideoRecordEventData eventArg, @NonNull Reply<Void> callback) {
2243+
BasicMessageChannel<Object> channel =
2244+
new BasicMessageChannel<>(
2245+
binaryMessenger,
2246+
"dev.flutter.pigeon.PendingRecordingFlutterApi.onVideoRecordingEvent",
2247+
getCodec());
2248+
channel.send(
2249+
new ArrayList<Object>(Collections.singletonList(eventArg)),
2250+
channelReply -> callback.reply(null));
2251+
}
21472252
}
21482253
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
21492254
public interface RecordingHostApi {
@@ -4027,6 +4132,8 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) {
40274132
return ResolutionInfo.fromList((ArrayList<Object>) readValue(buffer));
40284133
case (byte) 134:
40294134
return VideoQualityData.fromList((ArrayList<Object>) readValue(buffer));
4135+
case (byte) 135:
4136+
return VideoRecordEventData.fromList((ArrayList<Object>) readValue(buffer));
40304137
default:
40314138
return super.readValueOfType(type, buffer);
40324139
}
@@ -4055,6 +4162,9 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) {
40554162
} else if (value instanceof VideoQualityData) {
40564163
stream.write(134);
40574164
writeValue(stream, ((VideoQualityData) value).toList());
4165+
} else if (value instanceof VideoRecordEventData) {
4166+
stream.write(135);
4167+
writeValue(stream, ((VideoRecordEventData) value).toList());
40584168
} else {
40594169
super.writeValue(stream, value);
40604170
}

packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingFlutterApiImpl.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import androidx.camera.video.PendingRecording;
1010
import io.flutter.plugin.common.BinaryMessenger;
1111
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.PendingRecordingFlutterApi;
12+
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.VideoRecordEvent;
13+
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.VideoRecordEventData;
1214

1315
public class PendingRecordingFlutterApiImpl extends PendingRecordingFlutterApi {
1416
private final InstanceManager instanceManager;
@@ -22,4 +24,14 @@ public PendingRecordingFlutterApiImpl(
2224
void create(@NonNull PendingRecording pendingRecording, @Nullable Reply<Void> reply) {
2325
create(instanceManager.addHostCreatedInstance(pendingRecording), reply);
2426
}
27+
28+
void sendVideoRecordingFinalizedEvent(@NonNull Reply<Void> reply) {
29+
super.onVideoRecordingEvent(
30+
new VideoRecordEventData.Builder().setValue(VideoRecordEvent.FINALIZE).build(), reply);
31+
}
32+
33+
void sendVideoRecordingStartedEvent(@NonNull Reply<Void> reply) {
34+
super.onVideoRecordingEvent(
35+
new VideoRecordEventData.Builder().setValue(VideoRecordEvent.START).build(), reply);
36+
}
2537
}

packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingHostApiImpl.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public class PendingRecordingHostApiImpl implements PendingRecordingHostApi {
2424

2525
@VisibleForTesting @NonNull public CameraXProxy cameraXProxy = new CameraXProxy();
2626

27+
@VisibleForTesting PendingRecordingFlutterApiImpl pendingRecordingFlutterApi;
28+
2729
@VisibleForTesting SystemServicesFlutterApiImpl systemServicesFlutterApi;
2830

2931
@VisibleForTesting RecordingFlutterApiImpl recordingFlutterApi;
@@ -37,6 +39,8 @@ public PendingRecordingHostApiImpl(
3739
this.context = context;
3840
systemServicesFlutterApi = cameraXProxy.createSystemServicesFlutterApiImpl(binaryMessenger);
3941
recordingFlutterApi = new RecordingFlutterApiImpl(binaryMessenger, instanceManager);
42+
pendingRecordingFlutterApi =
43+
new PendingRecordingFlutterApiImpl(binaryMessenger, instanceManager);
4044
}
4145

4246
/** Sets the context, which is used to get the {@link Executor} needed to start the recording. */
@@ -73,10 +77,16 @@ public Executor getExecutor() {
7377
/**
7478
* Handles {@link VideoRecordEvent}s that come in during video recording. Sends any errors
7579
* encountered using {@link SystemServicesFlutterApiImpl}.
80+
*
81+
* <p>Currently only sends {@link VideoRecordEvent.Start} and {@link VideoRecordEvent.Finalize}
82+
* events to the Dart side.
7683
*/
7784
@VisibleForTesting
7885
public void handleVideoRecordEvent(@NonNull VideoRecordEvent event) {
79-
if (event instanceof VideoRecordEvent.Finalize) {
86+
if (event instanceof VideoRecordEvent.Start) {
87+
pendingRecordingFlutterApi.sendVideoRecordingStartedEvent(reply -> {});
88+
} else if (event instanceof VideoRecordEvent.Finalize) {
89+
pendingRecordingFlutterApi.sendVideoRecordingFinalizedEvent(reply -> {});
8090
VideoRecordEvent.Finalize castedEvent = (VideoRecordEvent.Finalize) event;
8191
if (castedEvent.hasError()) {
8292
String cameraErrorMessage;

packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PendingRecordingTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public class PendingRecordingTest {
4141
@Mock public RecordingFlutterApiImpl mockRecordingFlutterApi;
4242
@Mock public Context mockContext;
4343
@Mock public SystemServicesFlutterApiImpl mockSystemServicesFlutterApi;
44+
@Mock public PendingRecordingFlutterApiImpl mockPendingRecordingFlutterApi;
4445
@Mock public VideoRecordEvent.Finalize event;
4546
@Mock public Throwable throwable;
4647

@@ -80,6 +81,7 @@ public void testHandleVideoRecordEventSendsError() {
8081
PendingRecordingHostApiImpl pendingRecordingHostApi =
8182
new PendingRecordingHostApiImpl(mockBinaryMessenger, testInstanceManager, mockContext);
8283
pendingRecordingHostApi.systemServicesFlutterApi = mockSystemServicesFlutterApi;
84+
pendingRecordingHostApi.pendingRecordingFlutterApi = mockPendingRecordingFlutterApi;
8385
final String eventMessage = "example failure message";
8486

8587
when(event.hasError()).thenReturn(true);
@@ -89,9 +91,35 @@ public void testHandleVideoRecordEventSendsError() {
8991

9092
pendingRecordingHostApi.handleVideoRecordEvent(event);
9193

94+
verify(mockPendingRecordingFlutterApi).sendVideoRecordingFinalizedEvent(any());
9295
verify(mockSystemServicesFlutterApi).sendCameraError(eq(eventMessage), any());
9396
}
9497

98+
@Test
99+
public void handleVideoRecordEvent_SendsVideoRecordingFinalizedEvent() {
100+
PendingRecordingHostApiImpl pendingRecordingHostApi =
101+
new PendingRecordingHostApiImpl(mockBinaryMessenger, testInstanceManager, mockContext);
102+
pendingRecordingHostApi.pendingRecordingFlutterApi = mockPendingRecordingFlutterApi;
103+
104+
when(event.hasError()).thenReturn(false);
105+
106+
pendingRecordingHostApi.handleVideoRecordEvent(event);
107+
108+
verify(mockPendingRecordingFlutterApi).sendVideoRecordingFinalizedEvent(any());
109+
}
110+
111+
@Test
112+
public void handleVideoRecordEvent_SendsVideoRecordingStartedEvent() {
113+
PendingRecordingHostApiImpl pendingRecordingHostApi =
114+
new PendingRecordingHostApiImpl(mockBinaryMessenger, testInstanceManager, mockContext);
115+
pendingRecordingHostApi.pendingRecordingFlutterApi = mockPendingRecordingFlutterApi;
116+
VideoRecordEvent.Start mockStartEvent = mock(VideoRecordEvent.Start.class);
117+
118+
pendingRecordingHostApi.handleVideoRecordEvent(mockStartEvent);
119+
120+
verify(mockPendingRecordingFlutterApi).sendVideoRecordingStartedEvent(any());
121+
}
122+
95123
@Test
96124
public void flutterApiCreateTest() {
97125
final PendingRecordingFlutterApiImpl spyPendingRecordingFlutterApi =

packages/camera/camera_android_camerax/example/integration_test/integration_test.dart

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'package:camera_platform_interface/camera_platform_interface.dart';
1313
import 'package:flutter/painting.dart';
1414
import 'package:flutter_test/flutter_test.dart';
1515
import 'package:integration_test/integration_test.dart';
16+
import 'package:video_player/video_player.dart';
1617

1718
void main() {
1819
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -178,4 +179,81 @@ void main() {
178179
}
179180
}
180181
});
182+
183+
testWidgets('Video capture records valid video', (WidgetTester tester) async {
184+
final List<CameraDescription> cameras = await availableCameras();
185+
if (cameras.isEmpty) {
186+
return;
187+
}
188+
189+
final CameraController controller = CameraController(cameras[0],
190+
mediaSettings:
191+
const MediaSettings(resolutionPreset: ResolutionPreset.low));
192+
await controller.initialize();
193+
await controller.prepareForVideoRecording();
194+
195+
await controller.startVideoRecording();
196+
final int recordingStart = DateTime.now().millisecondsSinceEpoch;
197+
198+
sleep(const Duration(seconds: 2));
199+
200+
final XFile file = await controller.stopVideoRecording();
201+
final int postStopTime =
202+
DateTime.now().millisecondsSinceEpoch - recordingStart;
203+
204+
final File videoFile = File(file.path);
205+
final VideoPlayerController videoController = VideoPlayerController.file(
206+
videoFile,
207+
);
208+
await videoController.initialize();
209+
final int duration = videoController.value.duration.inMilliseconds;
210+
await videoController.dispose();
211+
212+
expect(duration, lessThan(postStopTime));
213+
});
214+
215+
testWidgets('Pause and resume video recording', (WidgetTester tester) async {
216+
final List<CameraDescription> cameras = await availableCameras();
217+
if (cameras.isEmpty) {
218+
return;
219+
}
220+
221+
final CameraController controller = CameraController(cameras[0],
222+
mediaSettings:
223+
const MediaSettings(resolutionPreset: ResolutionPreset.low));
224+
await controller.initialize();
225+
await controller.prepareForVideoRecording();
226+
227+
int startPause;
228+
int timePaused = 0;
229+
const int pauseIterations = 2;
230+
231+
await controller.startVideoRecording();
232+
final int recordingStart = DateTime.now().millisecondsSinceEpoch;
233+
sleep(const Duration(milliseconds: 500));
234+
235+
for (int i = 0; i < pauseIterations; i++) {
236+
await controller.pauseVideoRecording();
237+
startPause = DateTime.now().millisecondsSinceEpoch;
238+
sleep(const Duration(milliseconds: 500));
239+
await controller.resumeVideoRecording();
240+
timePaused += DateTime.now().millisecondsSinceEpoch - startPause;
241+
242+
sleep(const Duration(milliseconds: 500));
243+
}
244+
245+
final XFile file = await controller.stopVideoRecording();
246+
final int recordingTime =
247+
DateTime.now().millisecondsSinceEpoch - recordingStart;
248+
249+
final File videoFile = File(file.path);
250+
final VideoPlayerController videoController = VideoPlayerController.file(
251+
videoFile,
252+
);
253+
await videoController.initialize();
254+
final int duration = videoController.value.duration.inMilliseconds;
255+
await videoController.dispose();
256+
257+
expect(duration, lessThan(recordingTime - timePaused));
258+
});
181259
}

0 commit comments

Comments
 (0)