Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Commit

Permalink
[camera] Flash functionality for Android and iOS (#3314)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
4 people authored Dec 19, 2020
1 parent debfbec commit 22422af
Show file tree
Hide file tree
Showing 17 changed files with 542 additions and 45 deletions.
4 changes: 4 additions & 0 deletions packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.6.1

* Add flash support for Android and iOS implementations.

## 0.6.0+2

* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.CaptureFailure;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.OutputConfiguration;
import android.hardware.camera2.params.SessionConfiguration;
import android.hardware.camera2.params.StreamConfigurationMap;
Expand All @@ -34,6 +36,8 @@
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugins.camera.media.MediaRecorderBuilder;
import io.flutter.plugins.camera.types.FlashMode;
import io.flutter.plugins.camera.types.ResolutionPreset;
import io.flutter.view.TextureRegistry.SurfaceTextureEntry;
import java.io.File;
import java.io.FileOutputStream;
Expand Down Expand Up @@ -69,16 +73,8 @@ public class Camera {
private CamcorderProfile recordingProfile;
private int currentOrientation = ORIENTATION_UNKNOWN;
private Context applicationContext;

// Mirrors camera.dart
public enum ResolutionPreset {
low,
medium,
high,
veryHigh,
ultraHigh,
max,
}
private FlashMode flashMode;
private PictureCaptureRequest pictureCaptureRequest;

public Camera(
final Activity activity,
Expand All @@ -97,6 +93,7 @@ public Camera(
this.dartMessenger = dartMessenger;
this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
this.applicationContext = activity.getApplicationContext();
this.flashMode = FlashMode.auto;
orientationEventListener =
new OrientationEventListener(activity.getApplicationContext()) {
@Override
Expand Down Expand Up @@ -220,58 +217,125 @@ SurfaceTextureEntry getFlutterTexture() {
}

public void takePicture(@NonNull final Result result) {
// Only take 1 picture at a time
if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) {
result.error("captureAlreadyActive", "Picture is currently already being captured", null);
return;
}
// Store the result
this.pictureCaptureRequest = new PictureCaptureRequest(result);

// Create temporary file
final File outputDir = applicationContext.getCacheDir();
final File file;
try {
file = File.createTempFile("CAP", ".jpg", outputDir);
} catch (IOException | SecurityException e) {
result.error("cannotCreateFile", e.getMessage(), null);
pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null);
return;
}

// Listen for picture being taken
pictureImageReader.setOnImageAvailableListener(
reader -> {
try (Image image = reader.acquireLatestImage()) {
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
writeToFile(buffer, file);
result.success(file.getAbsolutePath());
pictureCaptureRequest.finish(file.getAbsolutePath());
} catch (IOException e) {
result.error("IOError", "Failed saving image", null);
pictureCaptureRequest.error("IOError", "Failed saving image", null);
}
},
null);

runPicturePreCapture();
}

private final CameraCaptureSession.CaptureCallback pictureCaptureCallback =
new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(
@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
assert (pictureCaptureRequest != null);
switch (pictureCaptureRequest.getState()) {
case awaitingPreCapture:
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
// Some devices might return null here, in which case we will also continue.
if (aeState == null
|| aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED
|| aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) {
runPictureCapture();
}
break;
}
}

@Override
public void onCaptureFailed(
@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull CaptureFailure failure) {
assert (pictureCaptureRequest != null);
String reason;
switch (failure.getReason()) {
case CaptureFailure.REASON_ERROR:
reason = "An error happened in the framework";
break;
case CaptureFailure.REASON_FLUSHED:
reason = "The capture has failed due to an abortCaptures() call";
break;
default:
reason = "Unknown reason";
}
pictureCaptureRequest.error("captureFailure", reason, null);
}
};

private void runPicturePreCapture() {
assert (pictureCaptureRequest != null);
pictureCaptureRequest.setState(PictureCaptureRequest.State.awaitingPreCapture);

captureRequestBuilder.set(
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
try {
cameraCaptureSession.capture(captureRequestBuilder.build(), pictureCaptureCallback, null);
captureRequestBuilder.set(
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE);
} catch (CameraAccessException e) {
pictureCaptureRequest.error("cameraAccess", e.getMessage(), null);
}
}

private void runPictureCapture() {
assert (pictureCaptureRequest != null);
pictureCaptureRequest.setState(PictureCaptureRequest.State.capturing);
try {
final CaptureRequest.Builder captureBuilder =
cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
captureBuilder.addTarget(pictureImageReader.getSurface());
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getMediaOrientation());

cameraCaptureSession.capture(
captureBuilder.build(),
new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureFailed(
@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull CaptureFailure failure) {
String reason;
switch (failure.getReason()) {
case CaptureFailure.REASON_ERROR:
reason = "An error happened in the framework";
break;
case CaptureFailure.REASON_FLUSHED:
reason = "The capture has failed due to an abortCaptures() call";
break;
default:
reason = "Unknown reason";
}
result.error("captureFailure", reason, null);
}
},
null);
switch (flashMode) {
case off:
captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
break;
case auto:
captureBuilder.set(
CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
break;
case always:
default:
captureBuilder.set(
CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
break;
}
cameraCaptureSession.capture(captureBuilder.build(), pictureCaptureCallback, null);
} catch (CameraAccessException e) {
result.error("cameraAccess", e.getMessage(), null);
pictureCaptureRequest.error("cameraAccess", e.getMessage(), null);
}
}

Expand Down Expand Up @@ -314,8 +378,7 @@ public void onConfigured(@NonNull CameraCaptureSession session) {
return;
}
cameraCaptureSession = session;
captureRequestBuilder.set(
CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
initPreviewCaptureBuilder();
cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null);
if (onSuccessCallback != null) {
onSuccessCallback.run();
Expand Down Expand Up @@ -452,6 +515,54 @@ public void resumeVideoRecording(@NonNull final Result result) {
result.success(null);
}

public void setFlashMode(@NonNull final Result result, FlashMode mode)
throws CameraAccessException {
// Get the flash availability
Boolean flashAvailable;
try {
flashAvailable =
cameraManager
.getCameraCharacteristics(cameraDevice.getId())
.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
} catch (CameraAccessException e) {
result.error("setFlashModeFailed", e.getMessage(), null);
return;
}
// Check if flash is available.
if (flashAvailable == null || !flashAvailable) {
result.error("setFlashModeFailed", "Device does not have flash capabilities", null);
return;
}
// Get flash

this.flashMode = mode;
initPreviewCaptureBuilder();
this.cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null);
result.success(null);
}

private void initPreviewCaptureBuilder() {
captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO);
switch (flashMode) {
case off:
captureRequestBuilder.set(
CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
break;
case auto:
captureRequestBuilder.set(
CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
break;
case always:
default:
captureRequestBuilder.set(
CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
break;
}
}

public void startPreview() throws CameraAccessException {
if (pictureImageReader == null || pictureImageReader.getSurface() == null) return;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.CamcorderProfile;
import android.util.Size;
import io.flutter.plugins.camera.Camera.ResolutionPreset;
import io.flutter.plugins.camera.types.ResolutionPreset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry;
import io.flutter.plugins.camera.types.FlashMode;
import io.flutter.view.TextureRegistry;
import java.util.HashMap;
import java.util.Map;
Expand Down Expand Up @@ -122,6 +123,21 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result)
camera.resumeVideoRecording(result);
break;
}
case "setFlashMode":
{
String modeStr = call.argument("mode");
FlashMode mode = FlashMode.getValueForString(modeStr);
if (mode == null) {
result.error("setFlashModeFailed", "Unknown flash mode " + modeStr, null);
return;
}
try {
camera.setFlashMode(result, mode);
} catch (Exception e) {
handleException(e, result);
}
break;
}
case "startImageStream":
{
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package io.flutter.plugins.camera;

import androidx.annotation.Nullable;
import io.flutter.plugin.common.MethodChannel;

class PictureCaptureRequest {

enum State {
idle,
awaitingPreCapture,
capturing,
finished,
error,
}

private final MethodChannel.Result result;
private State state;

public PictureCaptureRequest(MethodChannel.Result result) {
this.result = result;
state = State.idle;
}

public void setState(State state) {
if (isFinished()) throw new IllegalStateException("Request has already been finished");
this.state = state;
}

public State getState() {
return state;
}

public boolean isFinished() {
return state == State.finished || state == State.error;
}

public void finish(String absolutePath) {
if (isFinished()) throw new IllegalStateException("Request has already been finished");
result.success(absolutePath);
state = State.finished;
}

public void error(
String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) {
if (isFinished()) throw new IllegalStateException("Request has already been finished");
result.error(errorCode, errorMessage, errorDetails);
state = State.error;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.flutter.plugins.camera.types;

// Mirrors flash_mode.dart
public enum FlashMode {
off,
auto,
always;

public static FlashMode getValueForString(String modeStr) {
try {
return valueOf(modeStr);
} catch (IllegalArgumentException | NullPointerException e) {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.flutter.plugins.camera.types;

// Mirrors camera.dart
public enum ResolutionPreset {
low,
medium,
high,
veryHigh,
ultraHigh,
max,
}
Loading

0 comments on commit 22422af

Please sign in to comment.