快速开闭闪光灯实现与风险规避

背景

最近接手了一个有意思的需求,需求较为简单,在播放音乐时,在关键时间缀或时间段开闭闪光灯,实现音乐节奏和相机闪光联动。在Android上实现快速开闭闪光灯,避谈功耗问题,我们还需要考虑以下风险:

  • 避免APP丢帧,闪光和音乐的联动,必须异步线程处理;
  • 不能太过频发调用Camera,建议最小与系统vsync信号周期一致,即16ms;
  • Camera是共用的硬件资源,多摄像头设备上还需选取摄像头,使用中必须响应Camera或Torch回调;
  • 需要兼容手机厂商ROM包碎片化带来的一些问题,诸如魅族等设备与AOSP实现有出入;

在Android中,以Marshmallow(6.0)版本为界,开闭闪光有以下三种实现方案:

名称 适用系统版本 Camera 授权
surface方式 Marshmallow以下 不需要
preview方式 Marshmallow以下 需要
torchMode方式 Marshmallow及以上 不需要

表格中的适用版本并不绝对,比如我们碰到了魅族有个7.0机型只适用preview方式。方案实现代码不复杂,我们可以参考AOSP FlashlightController实现。本文记录在研发过程发现的问题,避免其他团队重复入局踩坑。

代码实现

使用闪光功能之前,我们要审明CAMERA和FLASHLIGHT权限、拿到可用Camera id以及监听Camera或Torch是否可用回调。
审明权限:

1
2
3
//相关权限审明
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FLASHLIGHT" />

获取可用Camera id:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//拿到具备闪光功能的后置摄像头Camera id
private String getCameraId(Context mContext) throws CameraAccessException {
CameraManager mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
String[] ids = mCameraManager.getCameraIdList();
for (String id : ids) {
CameraCharacteristics c = mCameraManager.getCameraCharacteristics(id);
Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
Integer lensFacing = c.get(CameraCharacteristics.LENS_FACING);
if (flashAvailable != null && flashAvailable
&& lensFacing != null && lensFacing == CameraCharacteristics.LENS_FACING_BACK) {
return id;
}
}
return null;
}

Android Marshmallow(6.0)以下监听Camera可用回调:

1
2
3
4
5
6
7
8
9
 mCameraManager.registerAvailabilityCallback(mAvailabilityCallback, mHandler);
/**
* Register a callback to be notified about camera device availability.
*
* @param callback the new callback to send camera availability notices to
* @param handler The handler on which the callback should be invoked, or {@code null} to use
* the current thread's {@link android.os.Looper looper}.
*/
public void registerAvailabilityCallback(@NonNull AvailabilityCallback callback,@Nullable Handler handler)

Android Marshmallow(6.0)及以上监听Torch可用回调:

1
2
3
4
5
6
7
8
9
mCameraManager.registerTorchCallback(mTorchCallback, mHandler);
/**
* Register a callback to be notified about torch mode status.
*
* @param callback The new callback to send torch mode status to
* @param handler The handler on which the callback should be invoked, or {@code null} to use
* the current thread's {@link android.os.Looper looper}.
*/
public void registerTorchCallback(@NonNull TorchCallback callback, @Nullable Handler handler)

Surface方式

Surface方式在快速开闭使用场景下,其实不太适合,存在Surface与Camera输入纹理的浪费,但部分机型只能通过此方案使用,比如OPPO 5.x有个机型。参考FlashlightController实现,启动需要调用CameraManager#openCamera、CameraDevice#createCaptureSession、CameraCaptureSession#capture三个方法。
CameraManager#openCamera:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private void startDevice() throws CameraAccessException {
//cameraid, CameraDevice.StateCallback, handler
mCameraManager.openCamera(getCameraId(), mCameraListener, mHandler);
}

/**
* Open a connection to a camera with the given ID.
*
* <p>Once the camera is successfully opened, {@link CameraDevice.StateCallback#onOpened} will
* be invoked with the newly opened {@link CameraDevice}. The camera device can then be set up
* for operation by calling {@link CameraDevice#createCaptureSession} and
* {@link CameraDevice#createCaptureRequest}</p>
*
* <!--
* <p>Since the camera device will be opened asynchronously, any asynchronous operations done
* on the returned CameraDevice instance will be queued up until the device startup has
* completed and the callback's {@link CameraDevice.StateCallback#onOpened onOpened} method is
* called. The pending operations are then processed in order.</p>
* -->
* <p>If the camera becomes disconnected during initialization
* after this function call returns,
* {@link CameraDevice.StateCallback#onDisconnected} with a
* {@link CameraDevice} in the disconnected state (and
* {@link CameraDevice.StateCallback#onOpened} will be skipped).</p>
*
* <p>If opening the camera device fails, then the device callback's
* {@link CameraDevice.StateCallback#onError onError} method will be called, and subsequent
* calls on the camera device will throw a {@link CameraAccessException}.</p>
*
* @param cameraId
* The unique identifier of the camera device to open
* @param callback
* The callback which is invoked once the camera is opened
* @param handler
* The handler on which the callback should be invoked, or
* {@code null} to use the current thread's {@link android.os.Looper looper}.
*/
@RequiresPermission(android.Manifest.permission.CAMERA)
public void openCamera(@NonNull String cameraId,
@NonNull final CameraDevice.StateCallback callback, @Nullable Handler handler)

这里不对openCamera调用说明讲解,有个风险提醒,Camera是一个异步调用,所有请求在Camera Device打开之前,都会入队,成功后会处理请求队列,快速闪光请求下,可能会有延时响应或丢失风险。
CameraDevice#createCaptureSession:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mSurface = new Surface(mSurfaceTexture);
ArrayList<Surface> outputs = new ArrayList<>(1);
outputs.add(mSurface);
mCameraDevice.createCaptureSession(outputs, mSessionListener, mHandler);

/**
* <p>Create a new camera capture session by providing the target output set of Surfaces to the
* camera device.</p>
*
* @param outputs The new set of Surfaces that should be made available as
* targets for captured image data.
* @param callback The callback to notify about the status of the new capture session.
* @param handler The handler on which the callback should be invoked, or {@code null} to use
* the current thread's {@link android.os.Looper looper}.
*/
public abstract void createCaptureSession(@NonNull List<Surface> outputs,
@NonNull CameraCaptureSession.StateCallback callback, @Nullable Handler handler)
throws CameraAccessException;

CameraCaptureSession#capture:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
CaptureRequest.Builder builder = mCameraDevice.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW);
builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH);
builder.addTarget(mSurface);
CaptureRequest request = builder.build();
mSession.capture(request, null, getUsableHandler());
mFlashlightRequest = request;

/**
* <p>Submit a request for an image to be captured by the camera device.</p>
*
* @param request the settings for this capture
* @param listener The callback object to notify once this request has been
* processed. If null, no metadata will be produced for this capture,
* although image data will still be produced.
* @param handler the handler on which the listener should be invoked, or
* {@code null} to use the current thread's {@link android.os.Looper
* looper}.
*
* @return int A unique capture sequence ID used by
* {@link CaptureCallback#onCaptureSequenceCompleted}.
*/
public abstract int capture(@NonNull CaptureRequest request,
@Nullable CaptureCallback listener, @Nullable Handler handler)
throws CameraAccessException;

关闭闪光灯较为简单,关闭CameraDivice,释放Surface实例等资源即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (mCameraDevice != null) {
mCameraDevice.close();
teardown();
}

private void teardown() {
mCameraDevice = null;
mSession = null;
mFlashlightRequest = null;
if (mSurface != null) {
mSurface.release();
mSurfaceTexture.release();
}
mSurface = null;
mSurfaceTexture = null;
}

Preview方式

相比Surface方式,Preview性能消耗较小,但需动态授权Camera,部分魅族手机适用此方案。开启闪光需要调用Camera#open和Camera#startPreview方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private fun turnOn() {
try {
if (mCamera == null) {
mCamera = Camera.open(mCameraId.toInt())
}
mCamera?.let {
var parameters = it.getParameters()
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH)
it.setParameters(parameters)
it.startPreview()
}
} catch (e: Throwable) {
dispatchError()
}
}
/**
* Creates a new Camera object to access a particular hardware camera. If
* the same camera is opened by other applications, this will throw a
* RuntimeException.
*
* <p>You must call {@link #release()} when you are done using the camera,
* otherwise it will remain locked and be unavailable to other applications.
*
* <p>Your application should only have one Camera object active at a time
* for a particular hardware camera.
*
* <p>Callbacks from other methods are delivered to the event loop of the
* thread which called open(). If this thread has no event loop, then
* callbacks are delivered to the main application event loop. If there
* is no main application event loop, callbacks are not delivered.
*
* <p class="caution"><b>Caution:</b> On some devices, this method may
* take a long time to complete. It is best to call this method from a
* worker thread (possibly using {@link android.os.AsyncTask}) to avoid
* blocking the main application UI thread.
*
* @param cameraId the hardware camera to access, between 0 and
* {@link #getNumberOfCameras()}-1.
* @return a new Camera object, connected, locked and ready for use.
*/
public static Camera open(int cameraId) {
return new Camera(cameraId);
}

关闭需要调用Camera#stopPreview和Camera#release方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private fun turnOff() {
try {
if (mCamera == null) {
return
}
mCamera?.let {
var parameters = it.getParameters()
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF)
it.setParameters(parameters)
it.stopPreview()
it.release()
}
} catch (e: Throwable) {
dispatchError()
} finally {
mCamera = null
}
}

TorchMode方式

TorchMode方式是Android在Marshmallow(6.0)及以上提供的闪光灯API,并且无需Camera动态授权,硬件响应及时。
参考FlashlightController实现,开闭只需调用CameraManager#setTorchMode方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 mCameraManager.setTorchMode(mCameraId, enabled);
/**
* Set the flash unit's torch mode of the camera of the given ID without opening the camera device.
*
* <p>Use {@link #getCameraIdList} to get the list of available camera devices and use
* {@link #getCameraCharacteristics} to check whether the camera device has a flash unit.
* Note that even if a camera device has a flash unit, turning on the torch mode may fail
* if the camera device or other camera resources needed to turn on the torch mode are in use.
* </p>
*
* <p> If {@link #setTorchMode} is called to turn on or off the torch mode successfully,
* {@link CameraManager.TorchCallback#onTorchModeChanged} will be invoked.
* However, even if turning on the torch mode is successful, the application does not have the
* exclusive ownership of the flash unit or the camera device. The torch mode will be turned
* off and becomes unavailable when the camera device that the flash unit belongs to becomes
* unavailable or when other camera resources to keep the torch on become unavailable (
* {@link CameraManager.TorchCallback#onTorchModeUnavailable} will be invoked). Also,
* other applications are free to call {@link #setTorchMode} to turn off the torch mode (
* {@link CameraManager.TorchCallback#onTorchModeChanged} will be invoked). If the latest
* application that turned on the torch mode exits, the torch mode will be turned off.
*
* @param cameraId
* The unique identifier of the camera device that the flash unit belongs to.
* @param enabled
* The desired state of the torch mode for the target camera device. Set to
* {@code true} to turn on the torch mode. Set to {@code false} to turn off the
* torch mode.
*/
public void setTorchMode(@NonNull String cameraId, boolean enabled)

结束语

我们在测试和灰度阶段收到部分不太理想的效果,这里列出:

  • 在实际测试过程发现,Marshmallow(6.0)以下部分设备,Surface和Preview方法均存在丢失或延时响应的现象,如果业务允许,建议可以直接屏蔽Marshmallow(6.0)以下设备;
  • Marshmallow(6.0)及以上,不是所有厂商都开启了Torch支持,例如魅族部分手机只支持Preview方式;
  • 国内部分厂商Lollipop(5.0)设备有自定义动态授权处理,调用前需要判断Camera是否禁用;
  • 国内部分厂商摄像头还有伸缩功能,快速开闭闪光灯,是一种很尴尬的使用方式;

快速开闭闪光灯功能存在功耗较大和持续适配机型等问题,如非必要,不太建议实现类似本文中的需求和使用场景。

参考

1.FlashlightController 6.0.1:http://androidxref.com/6.0.1_r10/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java
2.FlashlightController 5.1.1:http://androidxref.com/5.1.1_r6/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java