瞎搞scrcpy

最近有项目需要用到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. 鼠标只支持同显

瞎搞到此结束,谢谢观看。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值