最近有项目需要用到scrcpy,所以瞎搞玩一下。
首先的GitHub上下载了最新版本的windows系统的scrcpy以及源码。
下载地址:GitHub - Genymobile/scrcpy: Display and control your Android device
车机和电脑通过adb连接,scrcpy完美运行
流畅度和鼠标都非常OK。
上面是最常使用的方式,我们换一种使用方式,用scrcpy做辅助屏
要怎么实现呢,假如没有物理的第二块屏,那么我们只能创建虚拟屏幕
我在settings.apk中FallbackHome.java添加了部分代码,用于创建一个虚拟屏幕
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set ourselves totally black before the device is provisioned so that
// we don't flash the wallpaper before SUW
mProvisioned = Settings.Global.getInt(getContentResolver(),
Settings.Global.DEVICE_PROVISIONED, 0) != 0;
final int flags;
if (!mProvisioned) {
setTheme(R.style.FallbackHome_SetupWizard);
flags = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
} else {
flags = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
}
mWallManager = getSystemService(WallpaperManager.class);
if (mWallManager == null) {
Log.w(TAG, "Wallpaper manager isn't ready, can't listen to color changes!");
} else {
loadWallpaperColors(flags);
}
getWindow().getDecorView().setSystemUiVisibility(flags);
registerReceiver(mReceiver, new IntentFilter(Intent.ACTION_USER_UNLOCKED));
maybeFinish();
// ------------- cxj add for test virtual display -------------
final DisplayInfo displayInfo = new DisplayInfo();
getDisplay().getDisplayInfo(displayInfo);
int screenWidth = displayInfo.logicalWidth;
int screenHeight = displayInfo.logicalHeight;
int screenDesity = 240;
ImageReader mReader;
mReader = ImageReader.newInstance(screenWidth, screenHeight, PixelFormat.RGBA_8888, 2);
Surface surface = mReader.getSurface();
String virtualDisplayName = "snomy_display";
DisplayManager displayManager = this.getSystemService(DisplayManager.class);
VirtualDisplay virtualDisplay = displayManager.createVirtualDisplay(virtualDisplayName,
screenWidth, screenHeight, screenDesity,
surface,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC|DisplayManager.VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH);
// -------------------------------------------------------------------
}
为什么选在这里呢,因为开机它最先跑这里,还有settings.apk有android:sharedUserId="android.uid.system"权限。
在AndroidManifest.xml加上我们虚拟屏的录制权限
<!-- cxj add for Virtual Display -->
<uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT"/>
等系统运行起来后,我们通过dumpsys display 就可以看到两个显示设备了
Logical Displays: size=2
Display 0:
mDisplayId=0
mLayerStack=0
mHasContent=true
mAllowedDisplayModes=[1]
mRequestedColorMode=0
mDisplayOffset=(0, 0)
mDisplayScalingDisabled=false
mPrimaryDisplayDevice=内置屏幕
mBaseDisplayInfo=DisplayInfo{"内置屏幕, displayId 0", uniqueId "local:0", app 1280 x 720, real 1280 x 720, largest app 1280 x 720, smallest app 1280 x 720, mode 1, defaultMode 1, modes [{id=1, width=1280, height=720, fps=57.670002}], colorMode 0, supportedColorModes [0], hdrCapabilities android.view.Display$HdrCapabilities@40f16308, rotation 0, density 240 (160.0 x 160.0) dpi, layerStack 0, appVsyncOff 8300000, presDeadline 10040038, type BUILT_IN, address {port=0}, state ON, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, removeMode 0}
mOverrideDisplayInfo=DisplayInfo{"内置屏幕, displayId 0", uniqueId "local:0", app 1280 x 720, real 1280 x 720, largest app 1280 x 1225, smallest app 720 x 665, mode 1, defaultMode 1, modes [{id=1, width=1280, height=720, fps=57.670002}], colorMode 0, supportedColorModes [0], hdrCapabilities android.view.Display$HdrCapabilities@40f16308, rotation 0, density 240 (160.0 x 160.0) dpi, layerStack 0, appVsyncOff 8300000, presDeadline 10040038, type BUILT_IN, address {port=0}, state ON, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, removeMode 0}
Display 1:
mDisplayId=1
mLayerStack=1
mHasContent=false
mAllowedDisplayModes=[2]
mRequestedColorMode=0
mDisplayOffset=(0, 0)
mDisplayScalingDisabled=false
mPrimaryDisplayDevice=snomy_display
mBaseDisplayInfo=DisplayInfo{"snomy_display, displayId 1", uniqueId "virtual:com.android.settings,1000,snomy_display,0", app 1280 x 720, real 1280 x 720, largest app 1280 x 720, smallest app 1280 x 720, mode 2, defaultMode 2, modes [{id=2, width=1280, height=720, fps=60.0}], colorMode 0, supportedColorModes [0], hdrCapabilities null, rotation 0, density 240 (240.0 x 240.0) dpi, layerStack 1, appVsyncOff 0, presDeadline 16666666, type VIRTUAL, state ON, owner com.android.settings (uid 1000), removeMode 0}
mOverrideDisplayInfo=DisplayInfo{"snomy_display, displayId 1", uniqueId "virtual:com.android.settings,1000,snomy_display,0", app 1280 x 720, real 1280 x 720, largest app 1280 x 1280, smallest app 720 x 720, mode 2, defaultMode 2, modes [{id=2, width=1280, height=720, fps=60.0}], colorMode 0, supportedColorModes [0], hdrCapabilities null, rotation 0, density 240 (240.0 x 240.0) dpi, layerStack 1, appVsyncOff 0, presDeadline 16666666, type VIRTUAL, state ON, owner com.android.settings (uid 1000), removeMode 0}
此时我们运行scrcpy.exe --displayid 1 ,作用就是scrcpy去抓取我们指定的虚拟屏。
想着很理想,可是现实很残酷,居然黑屏,发生了什么!!!!!!!
通过研究scrcpy的代码发现,它是用surfacecontrol指定layerStack去实现的。
当虚拟屏的mHasContent=false的时候是没有内容的。
我们使用adb命令, 启动一个activity到虚拟屏去
am start --display 1 -n com.google.android.apps.maps/com.google.android.maps.MapsActivity
此时,scrcpy能完美现实,鼠标也有用,嗯,非常不错。
也就是说,在这种方式下只能双屏异显,那要实现双屏同显异显都支持呢,这怎么搞!
方法还是有的,需要改点源码。
通过研究发现,scrcpy是将一个scrcpy-server,push到android,然后运行app_procss运行的。
scrcpy-server其实就是一个apk,通过源码可以编译(编译过程很艰辛,主要是android studio版本太旧导致的,升级到最新的版本编译就顺利的很,基本一把过)
要实现双屏同显异显,我们只要修改scrcpy的抓取方式就可以了,scrcpy-serve不要通过surfaceControl去实现,而是通过创建一个虚拟屏去实现抓取,这样我们前面的那个创建虚拟屏的过程就不需要了,在没有内容虚拟屏的时候就能同显主屏,有内容的时候就实现了异显,嗯,想法很好(实际上有点难,原始的代码不支持android10,搞了很久,不多说了,直接上代码吧,写累了)
在scrcpy源码中修改scrcpy-master\server\src\main\java\com\genymobile\scrcpy\ScreenCapture.java
@Override
public void start(Surface surface) {
ScreenInfo screenInfo = device.getScreenInfo();
Rect contentRect = screenInfo.getContentRect();
// does not include the locked video orientation
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
int videoRotation = screenInfo.getVideoRotation();
int layerStack = device.getLayerStack();
if (display != null) {
SurfaceControl.destroyDisplay(display);
display = null;
}
if (virtualDisplay != null) {
virtualDisplay.release();
virtualDisplay = null;
}
// --------------- cxj modify -----------------
/*
try {
display = createDisplay();
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
Ln.d("Display: using SurfaceControl API");
} catch (Exception surfaceControlException) {
Rect videoRect = screenInfo.getVideoSize().toRect();
try {
virtualDisplay = ServiceManager.getDisplayManager()
.createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), device.getDisplayId(), surface);
Ln.d("Display: using DisplayManager API");
} catch (Exception displayManagerException) {
Ln.e("Could not create display using SurfaceControl", surfaceControlException);
Ln.e("Could not create display using DisplayManager", displayManagerException);
throw new AssertionError("Could not create display");
}
}*/
Rect videoRect = screenInfo.getVideoSize().toRect();
try {
virtualDisplay = ServiceManager.getDisplayManager()
.cxj_createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), 240, surface, 1|1<<6);
Ln.d("Display: using DisplayManager API");
} catch (Exception displayManagerException) {
Ln.e("Could not create display using DisplayManager", displayManagerException);
throw new AssertionError("Could not create display");
}
// --------------- cxj end -----------------
}
scrcpy-master\server\src\main\java\com\genymobile\scrcpy\wrappers\DisplayManager.java
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Command;
import com.genymobile.scrcpy.DisplayInfo;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.Size;
import android.annotation.SuppressLint;
import android.hardware.display.VirtualDisplay;
import android.view.Display;
import android.view.Surface;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
// cxj add
import android.content.Context;
import android.media.projection.MediaProjection;
import android.os.Handler;
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
public final class DisplayManager {
private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal
private Method createVirtualDisplayMethod;
static DisplayManager create() {
try {
Class<?> clazz = Class.forName("android.hardware.display.DisplayManagerGlobal");
Method getInstanceMethod = clazz.getDeclaredMethod("getInstance");
Object dmg = getInstanceMethod.invoke(null);
return new DisplayManager(dmg);
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
}
private DisplayManager(Object manager) {
this.manager = manager;
}
// public to call it from unit tests
public static DisplayInfo parseDisplayInfo(String dumpsysDisplayOutput, int displayId) {
Pattern regex = Pattern.compile(
"^ mOverrideDisplayInfo=DisplayInfo\\{\".*?, displayId " + displayId + ".*?(, FLAG_.*)?, real ([0-9]+) x ([0-9]+).*?, "
+ "rotation ([0-9]+).*?, layerStack ([0-9]+)",
Pattern.MULTILINE);
Matcher m = regex.matcher(dumpsysDisplayOutput);
if (!m.find()) {
return null;
}
int flags = parseDisplayFlags(m.group(1));
int width = Integer.parseInt(m.group(2));
int height = Integer.parseInt(m.group(3));
int rotation = Integer.parseInt(m.group(4));
int layerStack = Integer.parseInt(m.group(5));
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags);
}
private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) {
try {
String dumpsysDisplayOutput = Command.execReadOutput("dumpsys", "display");
return parseDisplayInfo(dumpsysDisplayOutput, displayId);
} catch (Exception e) {
Ln.e("Could not get display info from \"dumpsys display\" output", e);
return null;
}
}
private static int parseDisplayFlags(String text) {
Pattern regex = Pattern.compile("FLAG_[A-Z_]+");
if (text == null) {
return 0;
}
int flags = 0;
Matcher m = regex.matcher(text);
while (m.find()) {
String flagString = m.group();
try {
Field filed = Display.class.getDeclaredField(flagString);
flags |= filed.getInt(null);
} catch (ReflectiveOperationException e) {
// Silently ignore, some flags reported by "dumpsys display" are @TestApi
}
}
return flags;
}
public DisplayInfo getDisplayInfo(int displayId) {
try {
Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId);
if (displayInfo == null) {
// fallback when displayInfo is null
return getDisplayInfoFromDumpsysDisplay(displayId);
}
Class<?> cls = displayInfo.getClass();
// width and height already take the rotation into account
int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo);
int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo);
int rotation = cls.getDeclaredField("rotation").getInt(displayInfo);
int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo);
int flags = cls.getDeclaredField("flags").getInt(displayInfo);
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags);
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
}
public int[] getDisplayIds() {
try {
return (int[]) manager.getClass().getMethod("getDisplayIds").invoke(manager);
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
}
private Method getCreateVirtualDisplayMethod() throws NoSuchMethodException {
if (createVirtualDisplayMethod == null) {
createVirtualDisplayMethod = android.hardware.display.DisplayManager.class
.getMethod("createVirtualDisplay", String.class, int.class, int.class, int.class, Surface.class);
}
return createVirtualDisplayMethod;
}
public VirtualDisplay createVirtualDisplay(String name, int width, int height, int displayIdToMirror, Surface surface) throws Exception {
Method method = getCreateVirtualDisplayMethod();
return (VirtualDisplay) method.invoke(null, name, width, height, displayIdToMirror, surface);
}
// ------------------- cxj add ---------------------------
public VirtualDisplay cxj_createVirtualDisplay(String name, int width, int height, int densityDpi, Surface surface, int flags) throws Exception {
return (VirtualDisplay) manager.getClass()
.getMethod("createVirtualDisplay", Context.class, MediaProjection.class, String.class, int.class, int.class, int.class, Surface.class, int.class, VirtualDisplay.Callback.class, Handler.class, String.class)
.invoke(manager, null, null, "scrcpy", width, height, densityDpi, surface, flags, null, null, null);
}
// ------------------- cxj end ---------------------------
}
另外还需要修改android系统源码,不然有context的问题和权限问题
frameworks/base/core/java/android/hardware/display/DisplayManagerGlobal.java
public VirtualDisplay createVirtualDisplay(Context context, MediaProjection projection,
String name, int width, int height, int densityDpi, Surface surface, int flags,
VirtualDisplay.Callback callback, Handler handler, String uniqueId) {
if (TextUtils.isEmpty(name)) {
throw new IllegalArgumentException("name must be non-null and non-empty");
}
if (width <= 0 || height <= 0 || densityDpi <= 0) {
throw new IllegalArgumentException("width, height, and densityDpi must be "
+ "greater than 0");
}
VirtualDisplayCallback callbackWrapper = new VirtualDisplayCallback(callback, handler);
IMediaProjection projectionToken = projection != null ? projection.getProjection() : null;
int displayId;
try {
// -------------------- cxj modify for scrspy ------------------------
/*
displayId = mDm.createVirtualDisplay(callbackWrapper, projectionToken,
context.getPackageName(), name, width, height, densityDpi, surface, flags,
uniqueId);
*/
if (context == null)
{
displayId = mDm.createVirtualDisplay(callbackWrapper, projectionToken,
"snomy", name, width, height, densityDpi, surface, flags,
uniqueId);
}
else
{
displayId = mDm.createVirtualDisplay(callbackWrapper, projectionToken,
context.getPackageName(), name, width, height, densityDpi, surface, flags,
uniqueId);
}
// ---------------------------- cxj end -------------------------------------
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
if (displayId < 0) {
Log.e(TAG, "Could not create virtual display: " + name);
return null;
}
Display display = getRealDisplay(displayId);
if (display == null) {
Log.wtf(TAG, "Could not obtain display info for newly created "
+ "virtual display: " + name);
try {
mDm.releaseVirtualDisplay(callbackWrapper);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
return null;
}
return new VirtualDisplay(this, display, callbackWrapper, surface);
}
frameworks/base/services/core/java/com/android/server/display/DisplayManagerService.java
@Override // Binder call
public int createVirtualDisplay(IVirtualDisplayCallback callback,
IMediaProjection projection, String packageName, String name,
int width, int height, int densityDpi, Surface surface, int flags,
String uniqueId) {
final int callingUid = Binder.getCallingUid();
// cxj add "snomy" for scrcpy
if (!packageName.equals("snomy") && !validatePackageName(callingUid, packageName)) {
throw new SecurityException("packageName must match the calling uid");
}
if (callback == null) {
throw new IllegalArgumentException("appToken must not be null");
}
if (TextUtils.isEmpty(name)) {
throw new IllegalArgumentException("name must be non-null and non-empty");
}
if (width <= 0 || height <= 0 || densityDpi <= 0) {
throw new IllegalArgumentException("width, height, and densityDpi must be "
+ "greater than 0");
}
if (surface != null && surface.isSingleBuffered()) {
throw new IllegalArgumentException("Surface can't be single-buffered");
}
if ((flags & VIRTUAL_DISPLAY_FLAG_PUBLIC) != 0) {
flags |= VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR;
// Public displays can't be allowed to show content when locked.
if ((flags & VIRTUAL_DISPLAY_FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD) != 0) {
throw new IllegalArgumentException(
"Public display must not be marked as SHOW_WHEN_LOCKED_INSECURE");
}
}
if ((flags & VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY) != 0) {
flags &= ~VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR;
}
if (projection != null) {
try {
if (!getProjectionService().isValidMediaProjection(projection)) {
throw new SecurityException("Invalid media projection");
}
flags = projection.applyVirtualDisplayFlags(flags);
} catch (RemoteException e) {
throw new SecurityException("unable to validate media projection or flags");
}
}
// cxj add "snomy" for scrcpy
if (!packageName.equals("snomy") && callingUid != Process.SYSTEM_UID &&
(flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) {
if (!canProjectVideo(projection)) {
throw new SecurityException("Requires CAPTURE_VIDEO_OUTPUT or "
+ "CAPTURE_SECURE_VIDEO_OUTPUT permission, or an appropriate "
+ "MediaProjection token in order to create a screen sharing virtual "
+ "display.");
}
}
// cxj add "snomy" for scrcpy
if (!packageName.equals("snomy") && callingUid != Process.SYSTEM_UID && (flags & VIRTUAL_DISPLAY_FLAG_SECURE) != 0) {
if (!canProjectSecureVideo(projection)) {
throw new SecurityException("Requires CAPTURE_SECURE_VIDEO_OUTPUT "
+ "or an appropriate MediaProjection token to create a "
+ "secure virtual display.");
}
}
// Sometimes users can have sensitive information in system decoration windows. An app
// could create a virtual display with system decorations support and read the user info
// from the surface.
// We should only allow adding flag VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS
// to virtual displays that are owned by the system.
// cxj add "snomy" for scrcpy
if (!packageName.equals("snomy") && callingUid != Process.SYSTEM_UID
&& (flags & VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS) != 0) {
if (!checkCallingPermission(INTERNAL_SYSTEM_WINDOW, "createVirtualDisplay()")) {
throw new SecurityException("Requires INTERNAL_SYSTEM_WINDOW permission");
}
}
final long token = Binder.clearCallingIdentity();
try {
return createVirtualDisplayInternal(callback, projection, callingUid, packageName,
name, width, height, densityDpi, surface, flags, uniqueId);
} finally {
Binder.restoreCallingIdentity(token);
}
}
修改完这些,然后运行scrcpy,此时scrcpy自己创建了一个虚拟屏,如期运行,能实现双屏同显,异显。
但是不够完美,主要有两点:
1. 流畅度没前面那种方式高(难怪scrcpy默认要采用surfacecontrol方式录屏了)
2. 鼠标只支持同显
瞎搞到此结束,谢谢观看。