Skip to content

Commit 0b158bd

Browse files
BeMacizedmvanbeusekomdanielroekditman
authored
[camera] Add implementations for camera_platform_interface package. (flutter#3302)
* First suggestion for camera platform interface * Remove test coverage folder * Renamed onLatestImageAvailableHandler definition * Split CameraEvents into separate streams * Implemented & tested first parts of method channel implementation * Remove unused EventChannelMock class * Add missing unit tests * Added placeholders in default method channel implementation * Updated platform interface * Update packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart Co-authored-by: Maurits van Beusekom <[email protected]> * Add unit test for availableCameras * Expand availableCameras unit test. Added unit test for takePicture. * Add unit test for startVideoRecording * Add unit test for prepareForVideoRecording * Add unit test for stopVideoRecording * Add unit test for pauseVideoRecording * Add unit test for buildView * WIP: Dart and Android implementation * Fix formatting * Have resolution stream replay last value on subscription. Replace stream_transform with rxdart. * Added reverse method channel to replace event channel. Updated initialise and takePicture implementations for android. WIP implementation for startVideoRecording * Fixed example app for Android. Removed isRecordingVideo and isStreamingImages from buildView method. * Added some first tests for camera/camera * More tests and some feedback * iOS implementation: Removed standard event channel. Added reverse method channel. Updated initialize method. Added resolution changed event. Updated error reporting to use new method channel. * Started splitting initialize method * Finish splitting up initialize for iOS * Update unit tests * Fix takePicture method on iOS * Split initialize method on Android * Fix video recording on iOS. Updated platform interface. * Update unit tests * Update error handling of video methods in iOS code. Make iOS code more consistent. * Updated startVideoRecording documentation * Make sure file is returned by stopVideoRecording * Use correct event-type after initializing * Fix DartMessenger unit-tests * Change cast * Fix formatting * Fixed tests, formatting and analysis warnings * Added missing license to Dart files * Updated CHANGELOG and version * Added additional unit-tests to platform_interface * Added more tests * Formatted code * Re-added the CameraPreview widget * Use import/export instead of part implementation * fixed formatting * Resolved additional feedback * Update dependency to git repo * Depend on pub.dev for camera_platform_interface * Fix JAVA formatting * Fix changelog * Make sure camera package can be published * Assert when stream methods are called from wrong platform * Add dev_dependency on plugin_platform_interface package, required by tests. * Remove pedantic requirement from initialize() method. Remove unnecessary completers. * Remove dependency on dart:io * Restrict exposed types from platform interface * Moved test for image stream in separate file * Fixed formatting issue * Fix deprecation warning * Apply feedback from bparrishMines * Fix formatting issues * Removed redundant podspec files * Removed redundant ios files * Handle SecurityException Co-authored-by: Maurits van Beusekom <[email protected]> Co-authored-by: Maurits van Beusekom <[email protected]> Co-authored-by: daniel <[email protected]> Co-authored-by: David Iglesias Teixeira <[email protected]>
1 parent 0251017 commit 0b158bd

File tree

20 files changed

+1580
-928
lines changed

20 files changed

+1580
-928
lines changed

packages/camera/camera/CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
## 0.6.0
2+
3+
As part of implementing federated architecture and making the interface compatible with the web this version contains the following **breaking changes**:
4+
5+
Method changes in `CameraController`:
6+
- The `takePicture` method no longer accepts the `path` parameter, but instead returns the captured image as an instance of the `XFile` class;
7+
- The `startVideoRecording` method no longer accepts the `filePath`. Instead the recorded video is now returned as a `XFile` instance when the `stopVideoRecording` method completes;
8+
- The `stopVideoRecording` method now returns the captured video when it completes;
9+
- Added the `buildPreview` method which is now used to implement the CameraPreview widget.
10+
111
## 0.5.8+19
212

313
* Update Flutter SDK constraint.

packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java

+33-26
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,10 @@ public class Camera {
6565
private CaptureRequest.Builder captureRequestBuilder;
6666
private MediaRecorder mediaRecorder;
6767
private boolean recordingVideo;
68+
private File videoRecordingFile;
6869
private CamcorderProfile recordingProfile;
6970
private int currentOrientation = ORIENTATION_UNKNOWN;
71+
private Context applicationContext;
7072

7173
// Mirrors camera.dart
7274
public enum ResolutionPreset {
@@ -94,6 +96,7 @@ public Camera(
9496
this.flutterTexture = flutterTexture;
9597
this.dartMessenger = dartMessenger;
9698
this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
99+
this.applicationContext = activity.getApplicationContext();
97100
orientationEventListener =
98101
new OrientationEventListener(activity.getApplicationContext()) {
99102
@Override
@@ -135,7 +138,7 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException {
135138
}
136139

137140
@SuppressLint("MissingPermission")
138-
public void open(@NonNull final Result result) throws CameraAccessException {
141+
public void open() throws CameraAccessException {
139142
pictureImageReader =
140143
ImageReader.newInstance(
141144
captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2);
@@ -154,15 +157,13 @@ public void onOpened(@NonNull CameraDevice device) {
154157
try {
155158
startPreview();
156159
} catch (CameraAccessException e) {
157-
result.error("CameraAccess", e.getMessage(), null);
160+
dartMessenger.sendCameraErrorEvent(e.getMessage());
158161
close();
159162
return;
160163
}
161-
Map<String, Object> reply = new HashMap<>();
162-
reply.put("textureId", flutterTexture.id());
163-
reply.put("previewWidth", previewSize.getWidth());
164-
reply.put("previewHeight", previewSize.getHeight());
165-
result.success(reply);
164+
165+
dartMessenger.sendCameraInitializedEvent(
166+
previewSize.getWidth(), previewSize.getHeight());
166167
}
167168

168169
@Override
@@ -174,7 +175,7 @@ public void onClosed(@NonNull CameraDevice camera) {
174175
@Override
175176
public void onDisconnected(@NonNull CameraDevice cameraDevice) {
176177
close();
177-
dartMessenger.send(DartMessenger.EventType.ERROR, "The camera was disconnected.");
178+
dartMessenger.sendCameraErrorEvent("The camera was disconnected.");
178179
}
179180

180181
@Override
@@ -200,7 +201,7 @@ public void onError(@NonNull CameraDevice cameraDevice, int errorCode) {
200201
default:
201202
errorDescription = "Unknown camera error";
202203
}
203-
dartMessenger.send(DartMessenger.EventType.ERROR, errorDescription);
204+
dartMessenger.sendCameraErrorEvent(errorDescription);
204205
}
205206
},
206207
null);
@@ -218,12 +219,13 @@ SurfaceTextureEntry getFlutterTexture() {
218219
return flutterTexture;
219220
}
220221

221-
public void takePicture(String filePath, @NonNull final Result result) {
222-
final File file = new File(filePath);
223-
224-
if (file.exists()) {
225-
result.error(
226-
"fileExists", "File at path '" + filePath + "' already exists. Cannot overwrite.", null);
222+
public void takePicture(@NonNull final Result result) {
223+
final File outputDir = applicationContext.getCacheDir();
224+
final File file;
225+
try {
226+
file = File.createTempFile("CAP", ".jpg", outputDir);
227+
} catch (IOException | SecurityException e) {
228+
result.error("cannotCreateFile", e.getMessage(), null);
227229
return;
228230
}
229231

@@ -232,7 +234,7 @@ public void takePicture(String filePath, @NonNull final Result result) {
232234
try (Image image = reader.acquireLatestImage()) {
233235
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
234236
writeToFile(buffer, file);
235-
result.success(null);
237+
result.success(file.getAbsolutePath());
236238
} catch (IOException e) {
237239
result.error("IOError", "Failed saving image", null);
238240
}
@@ -308,8 +310,7 @@ private void createCaptureSession(
308310
public void onConfigured(@NonNull CameraCaptureSession session) {
309311
try {
310312
if (cameraDevice == null) {
311-
dartMessenger.send(
312-
DartMessenger.EventType.ERROR, "The camera was closed during configuration.");
313+
dartMessenger.sendCameraErrorEvent("The camera was closed during configuration.");
313314
return;
314315
}
315316
cameraCaptureSession = session;
@@ -320,14 +321,13 @@ public void onConfigured(@NonNull CameraCaptureSession session) {
320321
onSuccessCallback.run();
321322
}
322323
} catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) {
323-
dartMessenger.send(DartMessenger.EventType.ERROR, e.getMessage());
324+
dartMessenger.sendCameraErrorEvent(e.getMessage());
324325
}
325326
}
326327

327328
@Override
328329
public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
329-
dartMessenger.send(
330-
DartMessenger.EventType.ERROR, "Failed to configure camera session.");
330+
dartMessenger.sendCameraErrorEvent("Failed to configure camera session.");
331331
}
332332
};
333333

@@ -369,18 +369,24 @@ private void createCaptureSession(
369369
cameraDevice.createCaptureSession(surfaces, callback, null);
370370
}
371371

372-
public void startVideoRecording(String filePath, Result result) {
373-
if (new File(filePath).exists()) {
374-
result.error("fileExists", "File at path '" + filePath + "' already exists.", null);
372+
public void startVideoRecording(Result result) {
373+
final File outputDir = applicationContext.getCacheDir();
374+
try {
375+
videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir);
376+
} catch (IOException | SecurityException e) {
377+
result.error("cannotCreateFile", e.getMessage(), null);
375378
return;
376379
}
380+
377381
try {
378-
prepareMediaRecorder(filePath);
382+
prepareMediaRecorder(videoRecordingFile.getAbsolutePath());
379383
recordingVideo = true;
380384
createCaptureSession(
381385
CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface());
382386
result.success(null);
383387
} catch (CameraAccessException | IOException e) {
388+
recordingVideo = false;
389+
videoRecordingFile = null;
384390
result.error("videoRecordingFailed", e.getMessage(), null);
385391
}
386392
}
@@ -396,7 +402,8 @@ public void stopVideoRecording(@NonNull final Result result) {
396402
mediaRecorder.stop();
397403
mediaRecorder.reset();
398404
startPreview();
399-
result.success(null);
405+
result.success(videoRecordingFile.getAbsolutePath());
406+
videoRecordingFile = null;
400407
} catch (CameraAccessException | IllegalStateException e) {
401408
result.error("videoRecordingFailed", e.getMessage(), null);
402409
}

packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java

+34-27
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,56 @@
33
import android.text.TextUtils;
44
import androidx.annotation.Nullable;
55
import io.flutter.plugin.common.BinaryMessenger;
6-
import io.flutter.plugin.common.EventChannel;
6+
import io.flutter.plugin.common.MethodChannel;
77
import java.util.HashMap;
88
import java.util.Map;
99

1010
class DartMessenger {
11-
@Nullable private EventChannel.EventSink eventSink;
11+
@Nullable private MethodChannel channel;
1212

1313
enum EventType {
1414
ERROR,
1515
CAMERA_CLOSING,
16+
INITIALIZED,
1617
}
1718

18-
DartMessenger(BinaryMessenger messenger, long eventChannelId) {
19-
new EventChannel(messenger, "flutter.io/cameraPlugin/cameraEvents" + eventChannelId)
20-
.setStreamHandler(
21-
new EventChannel.StreamHandler() {
22-
@Override
23-
public void onListen(Object arguments, EventChannel.EventSink sink) {
24-
eventSink = sink;
25-
}
26-
27-
@Override
28-
public void onCancel(Object arguments) {
29-
eventSink = null;
30-
}
31-
});
19+
DartMessenger(BinaryMessenger messenger, long cameraId) {
20+
channel = new MethodChannel(messenger, "flutter.io/cameraPlugin/camera" + cameraId);
21+
}
22+
23+
void sendCameraInitializedEvent(Integer previewWidth, Integer previewHeight) {
24+
this.send(
25+
EventType.INITIALIZED,
26+
new HashMap<String, Object>() {
27+
{
28+
if (previewWidth != null) put("previewWidth", previewWidth.doubleValue());
29+
if (previewHeight != null) put("previewHeight", previewHeight.doubleValue());
30+
}
31+
});
3232
}
3333

3434
void sendCameraClosingEvent() {
35-
send(EventType.CAMERA_CLOSING, null);
35+
send(EventType.CAMERA_CLOSING);
3636
}
3737

38-
void send(EventType eventType, @Nullable String description) {
39-
if (eventSink == null) {
40-
return;
41-
}
38+
void sendCameraErrorEvent(@Nullable String description) {
39+
this.send(
40+
EventType.ERROR,
41+
new HashMap<String, Object>() {
42+
{
43+
if (!TextUtils.isEmpty(description)) put("description", description);
44+
}
45+
});
46+
}
47+
48+
void send(EventType eventType) {
49+
send(eventType, new HashMap<>());
50+
}
4251

43-
Map<String, String> event = new HashMap<>();
44-
event.put("eventType", eventType.toString().toLowerCase());
45-
// Only errors have a description.
46-
if (eventType == EventType.ERROR && !TextUtils.isEmpty(description)) {
47-
event.put("errorDescription", description);
52+
void send(EventType eventType, Map<String, Object> args) {
53+
if (channel == null) {
54+
return;
4855
}
49-
eventSink.success(event);
56+
channel.invokeMethod(eventType.toString().toLowerCase(), args);
5057
}
5158
}

packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java

+26-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import io.flutter.plugin.common.MethodChannel.Result;
1212
import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry;
1313
import io.flutter.view.TextureRegistry;
14+
import java.util.HashMap;
15+
import java.util.Map;
1416

1517
final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler {
1618
private final Activity activity;
@@ -49,11 +51,12 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result)
4951
handleException(e, result);
5052
}
5153
break;
52-
case "initialize":
54+
case "create":
5355
{
5456
if (camera != null) {
5557
camera.close();
5658
}
59+
5760
cameraPermissions.requestPermissions(
5861
activity,
5962
permissionsRegistry,
@@ -69,12 +72,28 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result)
6972
result.error(errCode, errDesc, null);
7073
}
7174
});
72-
75+
break;
76+
}
77+
case "initialize":
78+
{
79+
if (camera != null) {
80+
try {
81+
camera.open();
82+
result.success(null);
83+
} catch (Exception e) {
84+
handleException(e, result);
85+
}
86+
} else {
87+
result.error(
88+
"cameraNotFound",
89+
"Camera not found. Please call the 'create' method before calling 'initialize'.",
90+
null);
91+
}
7392
break;
7493
}
7594
case "takePicture":
7695
{
77-
camera.takePicture(call.argument("path"), result);
96+
camera.takePicture(result);
7897
break;
7998
}
8099
case "prepareForVideoRecording":
@@ -85,7 +104,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result)
85104
}
86105
case "startVideoRecording":
87106
{
88-
camera.startVideoRecording(call.argument("filePath"), result);
107+
camera.startVideoRecording(result);
89108
break;
90109
}
91110
case "stopVideoRecording":
@@ -157,7 +176,9 @@ private void instantiateCamera(MethodCall call, Result result) throws CameraAcce
157176
resolutionPreset,
158177
enableAudio);
159178

160-
camera.open(result);
179+
Map<String, Object> reply = new HashMap<>();
180+
reply.put("cameraId", flutterSurfaceTexture.id());
181+
result.success(reply);
161182
}
162183

163184
// We move catching CameraAccessException out of onMethodCall because it causes a crash

0 commit comments

Comments
 (0)