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

[camera] Added support for flash/torch mode in iOS and Android #2837

Closed
wants to merge 9 commits into from
Closed
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
3 changes: 3 additions & 0 deletions packages/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 0.5.9
* Added `enableTorch`, `disableTorch`, and `hasTorch` to `CameraController` to enable the use of the flash in torch mode (continuous on).

## 0.5.8+9

* Update android compileSdkVersion to 29.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public class Camera {
private final Size captureSize;
private final Size previewSize;
private final boolean enableAudio;
private final boolean flashSupported;
private boolean torchEnabled = false;

private CameraDevice cameraDevice;
private CameraCaptureSession cameraCaptureSession;
Expand Down Expand Up @@ -110,12 +112,40 @@ public void onOrientationChanged(int i) {
isFrontFacing =
characteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT;
ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset);
Boolean flashInfoAvailable = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
flashSupported = flashInfoAvailable == null ? false : flashInfoAvailable;
recordingProfile =
CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset);
captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight);
previewSize = computeBestPreviewSize(cameraName, preset);
}

// Will turn the torch on/off as long as the device and camera supports it
public void toggleTorch(boolean enable, @NonNull final Result result) {
try {
if (flashSupported) {
if (enable) {
captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);
torchEnabled = true;
} else {
captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
torchEnabled = false;
}
cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null);
result.success(null);
} else {
result.error("flashFailed", "Flash is not supported on this device", "");
}
} catch (CameraAccessException e) {
result.error("cameraAccess", e.getMessage(), null);
}
}

// Returns true if camera supports the torch
public void hasTorch(@NonNull final Result result) {
result.success(flashSupported);
}

private void prepareMediaRecorder(String outputFilePath) throws IOException {
if (mediaRecorder != null) {
mediaRecorder.release();
Expand Down Expand Up @@ -281,6 +311,11 @@ private void createCaptureSession(
// Create a new capture builder.
captureRequestBuilder = cameraDevice.createCaptureRequest(templateType);

// When starting a video recording, re-enable flash torch if we had it enabled before starting
if (torchEnabled) {
captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);
}

// Build Flutter surface to render to
SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture();
surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,33 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result)
}
break;
}
case "enableTorch":
{
try {
camera.toggleTorch(true, result);
} catch (Exception e) {
handleException(e, result);
}
break;
}
case "disableTorch":
{
try {
camera.toggleTorch(false, result);
} catch (Exception e) {
handleException(e, result);
}
break;
}
case "hasTorch":
{
try {
camera.hasTorch(result);
} catch (Exception e) {
handleException(e, result);
}
break;
}
case "dispose":
{
if (camera != null) {
Expand Down
1 change: 1 addition & 0 deletions packages/camera/example/ios/Flutter/.last_build_id
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ca4deb5e1e4d8fdaf00843cb47ee20c7
18 changes: 18 additions & 0 deletions packages/camera/example/ios/Flutter/Flutter.podspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#
# NOTE: This podspec is NOT to be published. It is only used as a local source!
#

Pod::Spec.new do |s|
s.name = 'Flutter'
s.version = '1.0.0'
s.summary = 'High-performance, high-fidelity mobile apps.'
s.description = <<-DESC
Flutter provides an easy and productive way to build and deploy high-performance mobile apps for Android and iOS.
DESC
s.homepage = 'https://flutter.io'
s.license = { :type => 'MIT' }
s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s }
s.ios.deployment_target = '8.0'
s.vendored_frameworks = 'Flutter.framework'
end
19 changes: 4 additions & 15 deletions packages/camera/example/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
75201D617916C49BDEDF852A /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 620DDA07C00B5FF2F937CB5B /* libPods-Runner.a */; };
9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };
9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
Expand All @@ -28,8 +24,6 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -40,15 +34,13 @@
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
483D985F075B951ADBAD218E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
620DDA07C00B5FF2F937CB5B /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
Expand All @@ -63,8 +55,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
75201D617916C49BDEDF852A /* libPods-Runner.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -83,9 +73,7 @@
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B80C3931E831B6300D905FE /* App.framework */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEBA1CF902C7004384FC /* Flutter.framework */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
Expand Down Expand Up @@ -229,7 +217,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin";
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
3E30118C54AB12C3EB9EDF27 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
Expand Down Expand Up @@ -269,9 +257,12 @@
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
"${PODS_ROOT}/../Flutter/Flutter.framework",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
Expand Down Expand Up @@ -315,7 +306,6 @@
/* Begin XCBuildConfiguration section */
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
Expand Down Expand Up @@ -372,7 +362,6 @@
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
44 changes: 42 additions & 2 deletions packages/camera/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
VideoPlayerController videoController;
VoidCallback videoPlayerListener;
bool enableAudio = true;
bool enableTorch = false;
bool torchSupported = false;

@override
void initState() {
Expand Down Expand Up @@ -102,7 +104,12 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
),
),
_captureControlRowWidget(),
_toggleAudioWidget(),
Row(
children: [
_toggleAudioWidget(),
_toggleTorchWidget(),
],
),
Padding(
padding: const EdgeInsets.all(5.0),
child: Row(
Expand Down Expand Up @@ -158,6 +165,36 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
);
}

/// Toggle torch mode
Widget _toggleTorchWidget() {
return Opacity(
opacity: torchSupported ? 1.0 : 0.2,
child: Padding(
padding: const EdgeInsets.only(left: 25),
child: Row(
children: <Widget>[
const Text('Toggle Torch:'),
Switch(
value: enableTorch,
onChanged: (bool value) async {
if (controller == null || !torchSupported) {
return;
}

setState(() => enableTorch = value);
if (enableTorch) {
await controller.enableTorch();
} else {
await controller.disableTorch();
}
},
),
],
),
),
);
}

/// Display the thumbnail of the captured image or video.
Widget _thumbnailWidget() {
return Expanded(
Expand Down Expand Up @@ -237,7 +274,7 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
controller.value.isRecordingVideo
? onStopButtonPressed
: null,
)
),
],
);
}
Expand Down Expand Up @@ -278,6 +315,7 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
void onNewCameraSelected(CameraDescription cameraDescription) async {
if (controller != null) {
await controller.dispose();
setState(() => enableTorch = false);
}
controller = CameraController(
cameraDescription,
Expand All @@ -295,6 +333,8 @@ class _CameraExampleHomeState extends State<CameraExampleHome>

try {
await controller.initialize();
final hasTorch = await controller.hasTorch();
setState(() => torchSupported = hasTorch);
} on CameraException catch (e) {
_showCameraException(e);
}
Expand Down
45 changes: 45 additions & 0 deletions packages/camera/ios/Classes/CameraPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ @interface FLTCam : NSObject <FlutterTexture,
@property(assign, nonatomic) BOOL audioIsDisconnected;
@property(assign, nonatomic) BOOL isAudioSetup;
@property(assign, nonatomic) BOOL isStreamingImages;
@property(assign, nonatomic) BOOL isTorchEnabled;
@property(assign, nonatomic) ResolutionPreset resolutionPreset;
@property(assign, nonatomic) CMTime lastVideoSampleTime;
@property(assign, nonatomic) CMTime lastAudioSampleTime;
Expand Down Expand Up @@ -656,6 +657,32 @@ - (void)stopImageStream {
}
}

- (void)toggleTorch:(bool)enabled :(FlutterResult)result :(AVCaptureDevice *)device {
NSLog(@"[toggleTorch] Calling with enabled: %s _isTorchEnabled: %s",
enabled == true ? "true" : "false", _isTorchEnabled == true ? "true" : "false");
if ([device hasTorch]) {
[device lockForConfiguration:nil];
if (!enabled) {
[device setTorchMode:AVCaptureTorchModeOff];
_isTorchEnabled = false;
result(nil);
} else {
NSError *anyError;
BOOL success = [device setTorchModeOnWithLevel:AVCaptureMaxAvailableTorchLevel
error:&anyError];
[device unlockForConfiguration];
if (!success) {
result(getFlutterError(anyError));
} else {
_isTorchEnabled = true;
result(nil);
}
}
} else {
result([FlutterError errorWithCode:@"UNAVAILABLE" message:@"Torch is unavailable" details:nil]);
}
}

- (BOOL)setupWriterForPath:(NSString *)path {
NSError *error = nil;
NSURL *outputURL;
Expand Down Expand Up @@ -713,6 +740,14 @@ - (BOOL)setupWriterForPath:(NSString *)path {
[_audioOutput setSampleBufferDelegate:self queue:_dispatchQueue];
}

// When starting video capture the torch will be turned off, so re-enable it here so it's started
// in time for recording to start.
if (_isTorchEnabled) {
[self.captureDevice lockForConfiguration:nil];
[self.captureDevice setTorchModeOnWithLevel:AVCaptureMaxAvailableTorchLevel error:nil];
[self.captureDevice unlockForConfiguration];
}

[_videoWriter addInput:_videoWriterInput];
[_captureVideoOutput setSampleBufferDelegate:self queue:_dispatchQueue];

Expand Down Expand Up @@ -819,6 +854,16 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re
} else {
result(FlutterMethodNotImplemented);
}
} else if ([@"enableTorch" isEqualToString:call.method]) {
[_camera toggleTorch:true:result:_camera.captureDevice];
} else if ([@"disableTorch" isEqualToString:call.method]) {
[_camera toggleTorch:false:result:_camera.captureDevice];
} else if ([@"hasTorch" isEqualToString:call.method]) {
if ([_camera.captureDevice hasTorch]) {
result(@(YES));
} else {
result(@(NO));
}
} else if ([@"initialize" isEqualToString:call.method]) {
NSString *cameraName = call.arguments[@"cameraName"];
NSString *resolutionPreset = call.arguments[@"resolutionPreset"];
Expand Down
Loading