Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get camera list from camera service directly #5669

Closed
wants to merge 1 commit 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
1 change: 1 addition & 0 deletions server/build_without_gradle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ cd "$SERVER_DIR/src/main/aidl"
# Fake sources to expose hidden Android types to the project
FAKE_SRC=( \
android/content/*java \
android/hardware/*java \
)

SRC=( \
Expand Down
14 changes: 14 additions & 0 deletions server/src/main/java/android/hardware/ICameraServiceListener.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package android.hardware;

import android.os.IBinder;

public interface ICameraServiceListener {
int STATUS_NOT_PRESENT = 0;
int STATUS_ENUMERATING = 2;

class Default implements ICameraServiceListener {
public IBinder asBinder() {
return null;
}
}
}
94 changes: 60 additions & 34 deletions server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.video.VideoCodec;
import com.genymobile.scrcpy.wrappers.CameraService;
import com.genymobile.scrcpy.wrappers.DisplayManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;

Expand All @@ -23,6 +24,7 @@
import android.os.Build;
import android.util.Range;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
Expand Down Expand Up @@ -122,56 +124,80 @@ private static String getCameraFacingName(int facing) {

public static String buildCameraListMessage(boolean includeSizes) {
StringBuilder builder = new StringBuilder("List of cameras:");
CameraService cameraService = ServiceManager.getCameraService();
CameraManager cameraManager = ServiceManager.getCameraManager();
try {
String[] cameraIds = cameraManager.getCameraIdList();
String[] cameraIds = cameraService.getCameraIdList();
if (cameraIds == null || cameraIds.length == 0) {
builder.append("\n (none)");
} else {
for (String id : cameraIds) {
builder.append("\n --camera-id=").append(id);
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
try {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);

int facing = characteristics.get(CameraCharacteristics.LENS_FACING);
builder.append(" (").append(getCameraFacingName(facing)).append(", ");
int[] capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
if (capabilities == null) {
continue;
}
// Ignore depth cameras as suggested by official documentation
// <https://developer.android.com/media/camera/camera2/camera-enumeration>
boolean isBackwardCompatible = false;
for (int capability : capabilities) {
if (capability == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) {
isBackwardCompatible = true;
break;
}
}
if (!isBackwardCompatible) {
continue;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should do that even with the CameraManager then (the current implementation), correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. On my Xiaomi devices, there are two invalid cameras ("facing" is "external" and fails to open) that can be filtered by this capability. However, they are hidden in CameraManager.getCameraIdList. I don't know if any OS returns non-backward-compatible cameras from CameraManager

}

Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
builder.append(activeSize.width()).append("x").append(activeSize.height());
builder.append("\n --camera-id=").append(id);

try {
// Capture frame rates for low-FPS mode are the same for every resolution
Range<Integer>[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
SortedSet<Integer> uniqueLowFps = getUniqueSet(lowFpsRanges);
builder.append(", fps=").append(uniqueLowFps);
} catch (Exception e) {
// Some devices may provide invalid ranges, causing an IllegalArgumentException "lower must be less than or equal to upper"
Ln.w("Could not get available frame rates for camera " + id, e);
}
int facing = characteristics.get(CameraCharacteristics.LENS_FACING);
builder.append(" (").append(getCameraFacingName(facing)).append(", ");

Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
builder.append(activeSize.width()).append("x").append(activeSize.height());

builder.append(')');
try {
// Capture frame rates for low-FPS mode are the same for every resolution
Range<Integer>[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
SortedSet<Integer> uniqueLowFps = getUniqueSet(lowFpsRanges);
builder.append(", fps=").append(uniqueLowFps);
} catch (Exception e) {
// Some devices may provide invalid ranges, causing an IllegalArgumentException "lower must be less than or equal to upper"
Ln.w("Could not get available frame rates for camera " + id, e);
}

if (includeSizes) {
StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
builder.append(')');

android.util.Size[] sizes = configs.getOutputSizes(MediaCodec.class);
if (sizes == null || sizes.length == 0) {
builder.append("\n (none)");
} else {
for (android.util.Size size : sizes) {
builder.append("\n - ").append(size.getWidth()).append('x').append(size.getHeight());
if (includeSizes) {
StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

android.util.Size[] sizes = configs.getOutputSizes(MediaCodec.class);
if (sizes == null || sizes.length == 0) {
builder.append("\n (none)");
} else {
for (android.util.Size size : sizes) {
builder.append("\n - ").append(size.getWidth()).append('x').append(size.getHeight());
}
}
}

android.util.Size[] highSpeedSizes = configs.getHighSpeedVideoSizes();
if (highSpeedSizes != null && highSpeedSizes.length > 0) {
builder.append("\n High speed capture (--camera-high-speed):");
for (android.util.Size size : highSpeedSizes) {
Range<Integer>[] highFpsRanges = configs.getHighSpeedVideoFpsRanges();
SortedSet<Integer> uniqueHighFps = getUniqueSet(highFpsRanges);
builder.append("\n - ").append(size.getWidth()).append("x").append(size.getHeight());
builder.append(" (fps=").append(uniqueHighFps).append(')');
android.util.Size[] highSpeedSizes = configs.getHighSpeedVideoSizes();
if (highSpeedSizes != null && highSpeedSizes.length > 0) {
builder.append("\n High speed capture (--camera-high-speed):");
for (android.util.Size size : highSpeedSizes) {
Range<Integer>[] highFpsRanges = configs.getHighSpeedVideoFpsRanges();
SortedSet<Integer> uniqueHighFps = getUniqueSet(highFpsRanges);
builder.append("\n - ").append(size.getWidth()).append("x").append(size.getHeight());
builder.append(" (fps=").append(uniqueHighFps).append(')');
}
}
}
} catch (IllegalArgumentException ignore) {
// Samsung devices might throw an IllegalArgumentException
// when getting camera characteristics for hidden cameras
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.genymobile.scrcpy.util.HandlerExecutor;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.wrappers.CameraService;
import com.genymobile.scrcpy.wrappers.ServiceManager;

import android.annotation.SuppressLint;
Expand Down Expand Up @@ -140,9 +141,10 @@ public void prepare() throws IOException {
}

private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException, ConfigurationException {
CameraService cameraService = ServiceManager.getCameraService();
CameraManager cameraManager = ServiceManager.getCameraManager();

String[] cameraIds = cameraManager.getCameraIdList();
String[] cameraIds = cameraService.getCameraIdList();
if (explicitCameraId != null) {
if (!Arrays.asList(cameraIds).contains(explicitCameraId)) {
Ln.e("Camera with id " + explicitCameraId + " not found\n" + LogUtils.buildCameraListMessage(false));
Expand All @@ -157,11 +159,16 @@ private static String selectCamera(String explicitCameraId, CameraFacing cameraF
}

for (String cameraId : cameraIds) {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
try {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);

int facing = characteristics.get(CameraCharacteristics.LENS_FACING);
if (cameraFacing.value() == facing) {
return cameraId;
int facing = characteristics.get(CameraCharacteristics.LENS_FACING);
if (cameraFacing.value() == facing) {
return cameraId;
}
} catch (IllegalArgumentException ignore) {
// Samsung devices might throw an IllegalArgumentException
// when getting camera characteristics for hidden cameras
}
}

Expand Down
110 changes: 110 additions & 0 deletions server/src/main/java/com/genymobile/scrcpy/wrappers/CameraService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.genymobile.scrcpy.wrappers;

import android.annotation.SuppressLint;
import android.hardware.ICameraServiceListener;
import android.os.Binder;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Parcel;
import android.util.Log;

import com.genymobile.scrcpy.util.Ln;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

public class CameraService {
private final IInterface service;

private Method addListenerMethod;
private Method removeListenerMethod;

public CameraService(IInterface service) {
this.service = service;
}

private Method getAddListenerMethod() throws NoSuchMethodException {
if (addListenerMethod == null) {
addListenerMethod = service.getClass().getDeclaredMethod("addListener", ICameraServiceListener.class);
}
return addListenerMethod;
}

public CameraStatus[] addListener(ICameraServiceListener listener) {
try {
Object[] rawStatuses = (Object[]) getAddListenerMethod().invoke(service, listener);
if (rawStatuses == null) {
return null;
}
CameraStatus[] cameraStatuses = new CameraStatus[rawStatuses.length];
for (int i = 0; i < rawStatuses.length; i++) {
cameraStatuses[i] = new CameraStatus(rawStatuses[i]);
}
return cameraStatuses;
} catch (Exception e) {
throw new RuntimeException(e);
}
}

private Method getRemoveListenerMethod() throws NoSuchMethodException {
if (removeListenerMethod == null) {
removeListenerMethod = service.getClass().getDeclaredMethod("removeListener", ICameraServiceListener.class);
}
return removeListenerMethod;
}

public void removeListener(ICameraServiceListener listener) {
try {
getRemoveListenerMethod().invoke(service, listener);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

// Simulate `CameraManager.getCameraIdList()`
// <https://android.googlesource.com/platform/frameworks/base/+/af274a1b252752b385bceb14f1f88e361e2c91e5/core/java/android/hardware/camera2/CameraManager.java#2351>
public String[] getCameraIdList() {
Binder binder = new Binder() {
@Override
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) {
return true;
}
};
ICameraServiceListener listener = new ICameraServiceListener.Default() {
// To immune from future changes to `ICameraServiceListener`,
// we use an empty `Binder` that ignores all transactions.
@Override
public IBinder asBinder() {
return binder;
}
};

CameraStatus[] cameraStatuses = addListener(listener);
removeListener(listener);

if (cameraStatuses == null) {
return null;
}

List<String> cameraIds = new ArrayList<>();
for (CameraStatus cameraStatus : cameraStatuses) {
if (cameraStatus.status != ICameraServiceListener.STATUS_NOT_PRESENT && cameraStatus.status != ICameraServiceListener.STATUS_ENUMERATING) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contrary to CameraManager.getCameraIdList(), shouldHideCamera() is not called. Is it on purpose to show camera that are wrongly hidden?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. shouldHideCamera was added recently, so for example when a companion device app added a virtual camera, it won't be seen by normal apps, I don't know if anyone is doing that, but it should be interesting to try to include them.

cameraIds.add(cameraStatus.cameraId);
}
}

return cameraIds.toArray(new String[0]);
}

@SuppressLint({"SoonBlockedPrivateApi", "PrivateApi"})
public static class CameraStatus {
public int status;
public String cameraId;

public CameraStatus(Object raw) throws NoSuchFieldException, IllegalAccessException {
status = raw.getClass().getDeclaredField("status").getInt(raw);
cameraId = (String) raw.getClass().getDeclaredField("cameraId").get(raw);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public final class ServiceManager {
private static StatusBarManager statusBarManager;
private static ClipboardManager clipboardManager;
private static ActivityManager activityManager;
private static CameraService cameraService;
private static CameraManager cameraManager;

private ServiceManager() {
Expand Down Expand Up @@ -97,6 +98,18 @@ public static ActivityManager getActivityManager() {
return activityManager;
}

public static CameraService getCameraService() {
if (cameraService == null) {
try {
IInterface camera = getService("media.camera", "android.hardware.ICameraService");
cameraService = new CameraService(camera);
} catch (Exception e) {
throw new AssertionError(e);
}
}
return cameraService;
}

public static CameraManager getCameraManager() {
if (cameraManager == null) {
try {
Expand Down