Compare commits

..

2 Commits

Author SHA1 Message Date
Johannes Neyer
74f11b096a Mention exclusive_caps mode in v4l2 documentation
PR #4435 <https://github.com/Genymobile/scrcpy/pull/4435>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-11-25 21:19:27 +01:00
Romain Vimont
25e33566f5 Mention turning off audio in camera documentation 2023-11-21 08:46:38 +01:00
5 changed files with 64 additions and 62 deletions

View File

@@ -18,6 +18,17 @@ scrcpy --video-source=display --audio-source=mic # force display AND micropho
scrcpy --video-source=camera --audio-source=output # force camera AND device audio output scrcpy --video-source=camera --audio-source=output # force camera AND device audio output
``` ```
Audio can be disabled:
```bash
# audio not captured at all
scrcpy --video-source=camera --no-audio
scrcpy --video-source=camera --no-audio --record=file.mp4
# audio captured and recorded, but not played
scrcpy --video-source=camera --no-audio-playback --record=file.mp4
```
## List ## List

View File

@@ -21,6 +21,13 @@ This will create a new video device in `/dev/videoN`, where `N` is an integer
(more [options](https://github.com/umlaeute/v4l2loopback#options) are available (more [options](https://github.com/umlaeute/v4l2loopback#options) are available
to create several devices or devices with specific IDs). to create several devices or devices with specific IDs).
If you encounter problems detecting your device with Chrome/WebRTC, you can try
`exclusive_caps` mode:
```
sudo modprobe v4l2loopback exclusive_caps=1
```
To list the enabled devices: To list the enabled devices:
```bash ```bash

View File

@@ -2,12 +2,11 @@ package com.genymobile.scrcpy;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.AttributionSource; import android.content.AttributionSource;
import android.content.Context; import android.content.MutableContextWrapper;
import android.content.ContextWrapper;
import android.os.Build; import android.os.Build;
import android.os.Process; import android.os.Process;
public final class FakeContext extends ContextWrapper { public final class FakeContext extends MutableContextWrapper {
public static final String PACKAGE_NAME = "com.android.shell"; public static final String PACKAGE_NAME = "com.android.shell";
public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29 public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29
@@ -19,7 +18,7 @@ public final class FakeContext extends ContextWrapper {
} }
private FakeContext() { private FakeContext() {
super(Workarounds.getSystemContext()); super(null);
} }
@Override @Override
@@ -45,9 +44,4 @@ public final class FakeContext extends ContextWrapper {
public int getDeviceId() { public int getDeviceId() {
return 0; return 0;
} }
@Override
public Context getApplicationContext() {
return this;
}
} }

View File

@@ -3,17 +3,14 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.DisplayManager;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.TargetApi;
import android.graphics.Rect; import android.graphics.Rect;
import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraManager;
import android.hardware.camera2.params.StreamConfigurationMap; import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.os.Build;
import android.util.Range; import android.util.Range;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
@@ -87,7 +84,6 @@ public final class LogUtils {
} }
} }
@TargetApi(Build.VERSION_CODES.P)
public static String buildCameraListMessage(boolean includeSizes) { public static String buildCameraListMessage(boolean includeSizes) {
StringBuilder builder = new StringBuilder("List of cameras:"); StringBuilder builder = new StringBuilder("List of cameras:");
CameraManager cameraManager = ServiceManager.getCameraManager(); CameraManager cameraManager = ServiceManager.getCameraManager();
@@ -97,7 +93,7 @@ public final class LogUtils {
builder.append("\n (none)"); builder.append("\n (none)");
} else { } else {
for (String id : cameraIds) { for (String id : cameraIds) {
builder.append("\n --camera-id=").append(id); builder.append("\n --video-source=camera --camera-id=").append(id);
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
int facing = characteristics.get(CameraCharacteristics.LENS_FACING); int facing = characteristics.get(CameraCharacteristics.LENS_FACING);
@@ -111,15 +107,6 @@ public final class LogUtils {
SortedSet<Integer> uniqueLowFps = getUniqueSet(lowFpsRanges); SortedSet<Integer> uniqueLowFps = getUniqueSet(lowFpsRanges);
builder.append("fps=").append(uniqueLowFps).append(')'); builder.append("fps=").append(uniqueLowFps).append(')');
int[] capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
if (Arrays.asList(capabilities).contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA)) {
builder.append("\n Logical multi-camera, backed by physical cameras:");
for (String phyId : characteristics.getPhysicalCameraIds()) {
CameraCharacteristics phyCharacteristics = cameraManager.getCameraCharacteristics(phyId);
appendPhysicalCamera(builder, phyId, phyCharacteristics);
}
}
if (includeSizes) { if (includeSizes) {
StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
@@ -147,13 +134,6 @@ public final class LogUtils {
return builder.toString(); return builder.toString();
} }
private static void appendPhysicalCamera(StringBuilder builder, String id, CameraCharacteristics characteristics) {
builder.append("\n --camera-id=").append(id).append(" (");
Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
builder.append(activeSize.width()).append("x").append(activeSize.height()).append(")");
}
private static SortedSet<Integer> getUniqueSet(Range<Integer>[] ranges) { private static SortedSet<Integer> getUniqueSet(Range<Integer>[] ranges) {
SortedSet<Integer> set = new TreeSet<>(); SortedSet<Integer> set = new TreeSet<>();
for (Range<Integer> range : ranges) { for (Range<Integer> range : ranges) {

View File

@@ -21,34 +21,18 @@ import java.lang.reflect.Method;
public final class Workarounds { public final class Workarounds {
private static final Class<?> ACTIVITY_THREAD_CLASS; private static Class<?> activityThreadClass;
private static final Object ACTIVITY_THREAD; private static Object activityThread;
static {
prepareMainLooper();
try {
// ActivityThread activityThread = new ActivityThread();
ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread");
Constructor<?> activityThreadConstructor = ACTIVITY_THREAD_CLASS.getDeclaredConstructor();
activityThreadConstructor.setAccessible(true);
ACTIVITY_THREAD = activityThreadConstructor.newInstance();
// ActivityThread.sCurrentActivityThread = activityThread;
Field sCurrentActivityThreadField = ACTIVITY_THREAD_CLASS.getDeclaredField("sCurrentActivityThread");
sCurrentActivityThreadField.setAccessible(true);
sCurrentActivityThreadField.set(null, ACTIVITY_THREAD);
} catch (Exception e) {
throw new AssertionError(e);
}
}
private Workarounds() { private Workarounds() {
// not instantiable // not instantiable
} }
public static void apply(boolean audio, boolean camera) { public static void apply(boolean audio, boolean camera) {
Workarounds.prepareMainLooper();
boolean mustFillAppInfo = false; boolean mustFillAppInfo = false;
boolean mustFillBaseContext = false;
boolean mustFillAppContext = false; boolean mustFillAppContext = false;
if (Build.BRAND.equalsIgnoreCase("meizu")) { if (Build.BRAND.equalsIgnoreCase("meizu")) {
@@ -69,6 +53,7 @@ public final class Workarounds {
// - <https://github.com/Genymobile/scrcpy/issues/4015#issuecomment-1595382142> // - <https://github.com/Genymobile/scrcpy/issues/4015#issuecomment-1595382142>
// - <https://github.com/Genymobile/scrcpy/issues/3805#issuecomment-1596148031> // - <https://github.com/Genymobile/scrcpy/issues/3805#issuecomment-1596148031>
mustFillAppInfo = true; mustFillAppInfo = true;
mustFillBaseContext = true;
mustFillAppContext = true; mustFillAppContext = true;
} }
@@ -81,12 +66,15 @@ public final class Workarounds {
if (camera) { if (camera) {
mustFillAppInfo = true; mustFillAppInfo = true;
mustFillAppContext = true; mustFillBaseContext = true;
} }
if (mustFillAppInfo) { if (mustFillAppInfo) {
Workarounds.fillAppInfo(); Workarounds.fillAppInfo();
} }
if (mustFillBaseContext) {
Workarounds.fillBaseContext();
}
if (mustFillAppContext) { if (mustFillAppContext) {
Workarounds.fillAppContext(); Workarounds.fillAppContext();
} }
@@ -105,9 +93,27 @@ public final class Workarounds {
Looper.prepareMainLooper(); Looper.prepareMainLooper();
} }
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
private static void fillActivityThread() throws Exception {
if (activityThread == null) {
// ActivityThread activityThread = new ActivityThread();
activityThreadClass = Class.forName("android.app.ActivityThread");
Constructor<?> activityThreadConstructor = activityThreadClass.getDeclaredConstructor();
activityThreadConstructor.setAccessible(true);
activityThread = activityThreadConstructor.newInstance();
// ActivityThread.sCurrentActivityThread = activityThread;
Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
sCurrentActivityThreadField.setAccessible(true);
sCurrentActivityThreadField.set(null, activityThread);
}
}
@SuppressLint("PrivateApi,DiscouragedPrivateApi") @SuppressLint("PrivateApi,DiscouragedPrivateApi")
private static void fillAppInfo() { private static void fillAppInfo() {
try { try {
fillActivityThread();
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); // ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData();
Class<?> appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData"); Class<?> appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData");
Constructor<?> appBindDataConstructor = appBindDataClass.getDeclaredConstructor(); Constructor<?> appBindDataConstructor = appBindDataClass.getDeclaredConstructor();
@@ -123,9 +129,9 @@ public final class Workarounds {
appInfoField.set(appBindData, applicationInfo); appInfoField.set(appBindData, applicationInfo);
// activityThread.mBoundApplication = appBindData; // activityThread.mBoundApplication = appBindData;
Field mBoundApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mBoundApplication"); Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
mBoundApplicationField.setAccessible(true); mBoundApplicationField.setAccessible(true);
mBoundApplicationField.set(ACTIVITY_THREAD, appBindData); mBoundApplicationField.set(activityThread, appBindData);
} catch (Throwable throwable) { } catch (Throwable throwable) {
// this is a workaround, so failing is not an error // this is a workaround, so failing is not an error
Ln.d("Could not fill app info: " + throwable.getMessage()); Ln.d("Could not fill app info: " + throwable.getMessage());
@@ -135,29 +141,33 @@ public final class Workarounds {
@SuppressLint("PrivateApi,DiscouragedPrivateApi") @SuppressLint("PrivateApi,DiscouragedPrivateApi")
private static void fillAppContext() { private static void fillAppContext() {
try { try {
Application app = new Application(); fillActivityThread();
Application app = Application.class.newInstance();
Field baseField = ContextWrapper.class.getDeclaredField("mBase"); Field baseField = ContextWrapper.class.getDeclaredField("mBase");
baseField.setAccessible(true); baseField.setAccessible(true);
baseField.set(app, FakeContext.get()); baseField.set(app, FakeContext.get());
// activityThread.mInitialApplication = app; // activityThread.mInitialApplication = app;
Field mInitialApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mInitialApplication"); Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
mInitialApplicationField.setAccessible(true); mInitialApplicationField.setAccessible(true);
mInitialApplicationField.set(ACTIVITY_THREAD, app); mInitialApplicationField.set(activityThread, app);
} catch (Throwable throwable) { } catch (Throwable throwable) {
// this is a workaround, so failing is not an error // this is a workaround, so failing is not an error
Ln.d("Could not fill app context: " + throwable.getMessage()); Ln.d("Could not fill app context: " + throwable.getMessage());
} }
} }
static Context getSystemContext() { private static void fillBaseContext() {
try { try {
Method getSystemContextMethod = ACTIVITY_THREAD_CLASS.getDeclaredMethod("getSystemContext"); fillActivityThread();
return (Context) getSystemContextMethod.invoke(ACTIVITY_THREAD);
Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
Context context = (Context) getSystemContextMethod.invoke(activityThread);
FakeContext.get().setBaseContext(context);
} catch (Throwable throwable) { } catch (Throwable throwable) {
// this is a workaround, so failing is not an error // this is a workaround, so failing is not an error
Ln.d("Could not get system context: " + throwable.getMessage()); Ln.d("Could not fill base context: " + throwable.getMessage());
return null;
} }
} }