Skip to content

Commit 40835fa

Browse files
BeMacizedmvanbeusekomdanielroek
authored andcommitted
[camera] Flash functionality for Android and iOS (flutter#3314)
* Move camera to camera/camera * Fix relative path after move * First suggestion for camera platform interface * Remove test coverage folder * Update the version to 1.0.0 * Remove redundant analysis overrides * Renamed onLatestImageAvailableHandler definition * Split CameraEvents into separate streams * Updated platform interface to have recording methods return XFile instances. * Update documentation and unit tests to match platform interface changes * Make file input optional for recording methods in platform interface. Update docs. * Add missing full stop in docs. * Run dartfmt. Wrapped docs after max 80 cols. Added missing full stop. * Implemented & tested first parts of method channel implementation * Remove unused EventChannelMock class * Add missing unit tests * Add availableCameras to 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]> * Update packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart Co-authored-by: Maurits van Beusekom <[email protected]> * Added placeholders in default method channel implementation * Add missing implementations to default method channel implementation * Fix formatting * Fix PR feedback * 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 * Remove TODO comment * Update packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart Co-authored-by: Maurits van Beusekom <[email protected]> * Update packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart Co-authored-by: Maurits van Beusekom <[email protected]> * Update packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart Co-authored-by: Maurits van Beusekom <[email protected]> * WIP: Dart and Android implementation * 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. * 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. * Added some first tests for camera/camera * Started splitting initialize method * More tests and some feedback * Finish splitting up initialize for iOS * Update unit tests * Remove unused listener in plugin * Fix takePicture method on iOS * 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. * Split initialize method on Android * Updated startVideoRecording documentation * Make sure file is returned by stopVideoRecording * Change cast * Use correct event-type after initializing * Fix DartMessenger unit-tests * Fix formatting * Fixed tests, formatting and analysis warnings * Added missing documentation public APIs * Added missing license to Dart files * Fix formatting issues * Updated CHANGELOG and version * Added more tests * Formatted code * Added additional unit-tests to platform_interface * Fix formatting issues * Re-added the CameraPreview widget * Refactored CameraException not to use * Use import/export instead of part implementation * fixed formatting * Resolved additional feedback * Resolved additional feedback * Flash WIP * Implement flash modes for Android * Update dependency to git repo * Add missing PictureCaptureRequest class * Add PR feedback * Move enums out of Camera.java * Expanded platform interface so support setting flash mode * Formatted dart code * Manually serialize flash mode enum rather than relying on stringification. * Add default to flash mode serialization * Fix for unit tests and reformatting * Fixed CHANGELOG and remove redundant iOS files * Expose FlashMode through camera package * Add reqeusted unit tests * Clean up new tests * Add unit tests for Android implementation * Update platform interface dependency to point to pub.dev release. Co-authored-by: Maurits van Beusekom <[email protected]> Co-authored-by: Maurits van Beusekom <[email protected]> Co-authored-by: daniel <[email protected]>
1 parent b9e4cd4 commit 40835fa

File tree

17 files changed

+542
-45
lines changed

17 files changed

+542
-45
lines changed

packages/camera/camera/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.6.1
2+
3+
* Add flash support for Android and iOS implementations.
4+
15
## 0.6.0+2
26

37
* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276))

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

Lines changed: 151 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import android.hardware.camera2.CameraMetadata;
1818
import android.hardware.camera2.CaptureFailure;
1919
import android.hardware.camera2.CaptureRequest;
20+
import android.hardware.camera2.CaptureResult;
21+
import android.hardware.camera2.TotalCaptureResult;
2022
import android.hardware.camera2.params.OutputConfiguration;
2123
import android.hardware.camera2.params.SessionConfiguration;
2224
import android.hardware.camera2.params.StreamConfigurationMap;
@@ -34,6 +36,8 @@
3436
import io.flutter.plugin.common.EventChannel;
3537
import io.flutter.plugin.common.MethodChannel.Result;
3638
import io.flutter.plugins.camera.media.MediaRecorderBuilder;
39+
import io.flutter.plugins.camera.types.FlashMode;
40+
import io.flutter.plugins.camera.types.ResolutionPreset;
3741
import io.flutter.view.TextureRegistry.SurfaceTextureEntry;
3842
import java.io.File;
3943
import java.io.FileOutputStream;
@@ -69,16 +73,8 @@ public class Camera {
6973
private CamcorderProfile recordingProfile;
7074
private int currentOrientation = ORIENTATION_UNKNOWN;
7175
private Context applicationContext;
72-
73-
// Mirrors camera.dart
74-
public enum ResolutionPreset {
75-
low,
76-
medium,
77-
high,
78-
veryHigh,
79-
ultraHigh,
80-
max,
81-
}
76+
private FlashMode flashMode;
77+
private PictureCaptureRequest pictureCaptureRequest;
8278

8379
public Camera(
8480
final Activity activity,
@@ -97,6 +93,7 @@ public Camera(
9793
this.dartMessenger = dartMessenger;
9894
this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
9995
this.applicationContext = activity.getApplicationContext();
96+
this.flashMode = FlashMode.auto;
10097
orientationEventListener =
10198
new OrientationEventListener(activity.getApplicationContext()) {
10299
@Override
@@ -220,58 +217,125 @@ SurfaceTextureEntry getFlutterTexture() {
220217
}
221218

222219
public void takePicture(@NonNull final Result result) {
220+
// Only take 1 picture at a time
221+
if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) {
222+
result.error("captureAlreadyActive", "Picture is currently already being captured", null);
223+
return;
224+
}
225+
// Store the result
226+
this.pictureCaptureRequest = new PictureCaptureRequest(result);
227+
228+
// Create temporary file
223229
final File outputDir = applicationContext.getCacheDir();
224230
final File file;
225231
try {
226232
file = File.createTempFile("CAP", ".jpg", outputDir);
227233
} catch (IOException | SecurityException e) {
228-
result.error("cannotCreateFile", e.getMessage(), null);
234+
pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null);
229235
return;
230236
}
231237

238+
// Listen for picture being taken
232239
pictureImageReader.setOnImageAvailableListener(
233240
reader -> {
234241
try (Image image = reader.acquireLatestImage()) {
235242
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
236243
writeToFile(buffer, file);
237-
result.success(file.getAbsolutePath());
244+
pictureCaptureRequest.finish(file.getAbsolutePath());
238245
} catch (IOException e) {
239-
result.error("IOError", "Failed saving image", null);
246+
pictureCaptureRequest.error("IOError", "Failed saving image", null);
240247
}
241248
},
242249
null);
243250

251+
runPicturePreCapture();
252+
}
253+
254+
private final CameraCaptureSession.CaptureCallback pictureCaptureCallback =
255+
new CameraCaptureSession.CaptureCallback() {
256+
@Override
257+
public void onCaptureCompleted(
258+
@NonNull CameraCaptureSession session,
259+
@NonNull CaptureRequest request,
260+
@NonNull TotalCaptureResult result) {
261+
assert (pictureCaptureRequest != null);
262+
switch (pictureCaptureRequest.getState()) {
263+
case awaitingPreCapture:
264+
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
265+
// Some devices might return null here, in which case we will also continue.
266+
if (aeState == null
267+
|| aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED
268+
|| aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) {
269+
runPictureCapture();
270+
}
271+
break;
272+
}
273+
}
274+
275+
@Override
276+
public void onCaptureFailed(
277+
@NonNull CameraCaptureSession session,
278+
@NonNull CaptureRequest request,
279+
@NonNull CaptureFailure failure) {
280+
assert (pictureCaptureRequest != null);
281+
String reason;
282+
switch (failure.getReason()) {
283+
case CaptureFailure.REASON_ERROR:
284+
reason = "An error happened in the framework";
285+
break;
286+
case CaptureFailure.REASON_FLUSHED:
287+
reason = "The capture has failed due to an abortCaptures() call";
288+
break;
289+
default:
290+
reason = "Unknown reason";
291+
}
292+
pictureCaptureRequest.error("captureFailure", reason, null);
293+
}
294+
};
295+
296+
private void runPicturePreCapture() {
297+
assert (pictureCaptureRequest != null);
298+
pictureCaptureRequest.setState(PictureCaptureRequest.State.awaitingPreCapture);
299+
300+
captureRequestBuilder.set(
301+
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
302+
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
303+
try {
304+
cameraCaptureSession.capture(captureRequestBuilder.build(), pictureCaptureCallback, null);
305+
captureRequestBuilder.set(
306+
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
307+
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE);
308+
} catch (CameraAccessException e) {
309+
pictureCaptureRequest.error("cameraAccess", e.getMessage(), null);
310+
}
311+
}
312+
313+
private void runPictureCapture() {
314+
assert (pictureCaptureRequest != null);
315+
pictureCaptureRequest.setState(PictureCaptureRequest.State.capturing);
244316
try {
245317
final CaptureRequest.Builder captureBuilder =
246318
cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
247319
captureBuilder.addTarget(pictureImageReader.getSurface());
248320
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getMediaOrientation());
249-
250-
cameraCaptureSession.capture(
251-
captureBuilder.build(),
252-
new CameraCaptureSession.CaptureCallback() {
253-
@Override
254-
public void onCaptureFailed(
255-
@NonNull CameraCaptureSession session,
256-
@NonNull CaptureRequest request,
257-
@NonNull CaptureFailure failure) {
258-
String reason;
259-
switch (failure.getReason()) {
260-
case CaptureFailure.REASON_ERROR:
261-
reason = "An error happened in the framework";
262-
break;
263-
case CaptureFailure.REASON_FLUSHED:
264-
reason = "The capture has failed due to an abortCaptures() call";
265-
break;
266-
default:
267-
reason = "Unknown reason";
268-
}
269-
result.error("captureFailure", reason, null);
270-
}
271-
},
272-
null);
321+
switch (flashMode) {
322+
case off:
323+
captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
324+
captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
325+
break;
326+
case auto:
327+
captureBuilder.set(
328+
CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
329+
break;
330+
case always:
331+
default:
332+
captureBuilder.set(
333+
CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
334+
break;
335+
}
336+
cameraCaptureSession.capture(captureBuilder.build(), pictureCaptureCallback, null);
273337
} catch (CameraAccessException e) {
274-
result.error("cameraAccess", e.getMessage(), null);
338+
pictureCaptureRequest.error("cameraAccess", e.getMessage(), null);
275339
}
276340
}
277341

@@ -314,8 +378,7 @@ public void onConfigured(@NonNull CameraCaptureSession session) {
314378
return;
315379
}
316380
cameraCaptureSession = session;
317-
captureRequestBuilder.set(
318-
CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
381+
initPreviewCaptureBuilder();
319382
cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null);
320383
if (onSuccessCallback != null) {
321384
onSuccessCallback.run();
@@ -452,6 +515,54 @@ public void resumeVideoRecording(@NonNull final Result result) {
452515
result.success(null);
453516
}
454517

518+
public void setFlashMode(@NonNull final Result result, FlashMode mode)
519+
throws CameraAccessException {
520+
// Get the flash availability
521+
Boolean flashAvailable;
522+
try {
523+
flashAvailable =
524+
cameraManager
525+
.getCameraCharacteristics(cameraDevice.getId())
526+
.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
527+
} catch (CameraAccessException e) {
528+
result.error("setFlashModeFailed", e.getMessage(), null);
529+
return;
530+
}
531+
// Check if flash is available.
532+
if (flashAvailable == null || !flashAvailable) {
533+
result.error("setFlashModeFailed", "Device does not have flash capabilities", null);
534+
return;
535+
}
536+
// Get flash
537+
538+
this.flashMode = mode;
539+
initPreviewCaptureBuilder();
540+
this.cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null);
541+
result.success(null);
542+
}
543+
544+
private void initPreviewCaptureBuilder() {
545+
captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO);
546+
switch (flashMode) {
547+
case off:
548+
captureRequestBuilder.set(
549+
CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
550+
captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
551+
break;
552+
case auto:
553+
captureRequestBuilder.set(
554+
CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
555+
captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
556+
break;
557+
case always:
558+
default:
559+
captureRequestBuilder.set(
560+
CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
561+
captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
562+
break;
563+
}
564+
}
565+
455566
public void startPreview() throws CameraAccessException {
456567
if (pictureImageReader == null || pictureImageReader.getSurface() == null) return;
457568

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import android.hardware.camera2.params.StreamConfigurationMap;
1111
import android.media.CamcorderProfile;
1212
import android.util.Size;
13-
import io.flutter.plugins.camera.Camera.ResolutionPreset;
13+
import io.flutter.plugins.camera.types.ResolutionPreset;
1414
import java.util.ArrayList;
1515
import java.util.Arrays;
1616
import java.util.Collections;

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import io.flutter.plugin.common.MethodChannel;
1111
import io.flutter.plugin.common.MethodChannel.Result;
1212
import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry;
13+
import io.flutter.plugins.camera.types.FlashMode;
1314
import io.flutter.view.TextureRegistry;
1415
import java.util.HashMap;
1516
import java.util.Map;
@@ -122,6 +123,21 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result)
122123
camera.resumeVideoRecording(result);
123124
break;
124125
}
126+
case "setFlashMode":
127+
{
128+
String modeStr = call.argument("mode");
129+
FlashMode mode = FlashMode.getValueForString(modeStr);
130+
if (mode == null) {
131+
result.error("setFlashModeFailed", "Unknown flash mode " + modeStr, null);
132+
return;
133+
}
134+
try {
135+
camera.setFlashMode(result, mode);
136+
} catch (Exception e) {
137+
handleException(e, result);
138+
}
139+
break;
140+
}
125141
case "startImageStream":
126142
{
127143
try {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package io.flutter.plugins.camera;
2+
3+
import androidx.annotation.Nullable;
4+
import io.flutter.plugin.common.MethodChannel;
5+
6+
class PictureCaptureRequest {
7+
8+
enum State {
9+
idle,
10+
awaitingPreCapture,
11+
capturing,
12+
finished,
13+
error,
14+
}
15+
16+
private final MethodChannel.Result result;
17+
private State state;
18+
19+
public PictureCaptureRequest(MethodChannel.Result result) {
20+
this.result = result;
21+
state = State.idle;
22+
}
23+
24+
public void setState(State state) {
25+
if (isFinished()) throw new IllegalStateException("Request has already been finished");
26+
this.state = state;
27+
}
28+
29+
public State getState() {
30+
return state;
31+
}
32+
33+
public boolean isFinished() {
34+
return state == State.finished || state == State.error;
35+
}
36+
37+
public void finish(String absolutePath) {
38+
if (isFinished()) throw new IllegalStateException("Request has already been finished");
39+
result.success(absolutePath);
40+
state = State.finished;
41+
}
42+
43+
public void error(
44+
String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) {
45+
if (isFinished()) throw new IllegalStateException("Request has already been finished");
46+
result.error(errorCode, errorMessage, errorDetails);
47+
state = State.error;
48+
}
49+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.flutter.plugins.camera.types;
2+
3+
// Mirrors flash_mode.dart
4+
public enum FlashMode {
5+
off,
6+
auto,
7+
always;
8+
9+
public static FlashMode getValueForString(String modeStr) {
10+
try {
11+
return valueOf(modeStr);
12+
} catch (IllegalArgumentException | NullPointerException e) {
13+
return null;
14+
}
15+
}
16+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.flutter.plugins.camera.types;
2+
3+
// Mirrors camera.dart
4+
public enum ResolutionPreset {
5+
low,
6+
medium,
7+
high,
8+
veryHigh,
9+
ultraHigh,
10+
max,
11+
}

0 commit comments

Comments
 (0)