scrcpy 类似软件实现思路与探索(初版)

背景

当初看到这个scrcpy工具觉得非常神奇,不知道怎么实现,我对某种业务具体实现特别感兴趣,但我对编程语言没有太多兴趣,对一些用不到算法也特别不感兴趣。后面自己研究一段时间,然后结合scrcpy源代码和其他产品大概实现思路,总结scrcpy 通过app_process实现技术。

技术

  1. app_process 技术
    这个是根本技术,自己实现adb命令功能代码,为什么我们不直接用adb 实现呢?以前一些云控技术用的adb 命令,因为adb 命令会创建进程,对于非常频繁的行为,性能太低。同时跨进程,没有直接源码实现。所以用app_process技术,因为可以通过反射实现adb 代码实现,那你会想我反射不一定要app_process,直接在android 代码反射也可以,反射代码可以写,但没有权限或者说权限不够,模拟点击调用会失败,通过源代码发现他需要系统的权限。如果不走app_process,可以root手机达到反射条件。
  2. 反射
    怎么反射,对于新手完全找不到方向,你看scrcpy代码,你也不知道为什么他们能找到,后面我想既然他们反射实现的adb命令,应该看了android 命令实现代码,果然我在 android 源代码找到有关代码,那么我们方向找到了,只要找命令源代码,那么基本就能反射出来。
  3. android 怎么编码或者怎么截图
    1. 反射截图命令
      我最开始也是这么想的,反射命令截图,FPS可以达到30帧样子,虽然达不到60fps基本作为协作工具完全可以了
    2. android h264 编码
      1. 这个通过编码成h264,获取一个buffer就投递给PC端【直播】
      2. 这里难点不熟悉android 编码代码,直接自己搜索一下这方便代码,完全写一个直播丢给PC,不用反射,编码直接申请权限,调用代码就可以了。
  4. 网络通信
    1. scrcpy用原生socket,然后协议用二进制自定义协议

      我觉得不是很有必要,如果真的为什么节约通信大小,直接用protobuf就好了,毕竟动态的varint还是比较节约内存,不过对于简单协议,用什么都可以,我只是提一嘴。

    2. 我自己用websocket+JSON 来通信

      不用自己切包,JSON解析扩展性高,对于命令来说,这一点协议没有消耗无所谓。

    3. 视频流,直接丢给单独接受视频端口,如果有服务接入,那么考虑东西会多一些。
  5. PC端
    1. 视频解码

      业界ffmepg 开发库,按照官方解码视频就好

    2. 渲染
      1. 网页

        感觉渲染速度达不到,貌似都是解决mp4 标签来做的,都会缓存,我没有试过,底层解码成图片,然后投递给网页,我最开始截图成PNG,然后给网页,貌似性能不错。

      2. SDL

        scrcpy就是这么写的,我网页慢,也用sdl,只是整个写法完全自己写的,没有借鉴scrcpy,因为他的跨平台而且用纯c,用大量函数指针来模拟面向对象写法,我直接用c++写的 windows代码,用原生win32 没有用任何其他的,mfc,或者 duilib或者其他的界面。

  6. 业务代码
    1. 点击实现
      1. android 反射实现代码
      2. PC 界面与 手机界面比例,然后计算在android 真实的坐标
    2. 按键实现
      1. androi 反射 input 命令代码
      2. SDL 处理按键操作
        1. 快捷键处理

          要判断什么时候快捷键,什么单独按键,所以这么设计一些业务代码蛮多,我自己重写几次才写好,所以没有相信那么简单。keyup keydown 什么时候处理,什么时候不处理。

        2. keydown可能够处理连续按键,keyup 不会触发,所以处理单独按键要响应keydown,而不是keyup触发,否则无法实现一个按键一直按着,触发多次逻辑,
  7. android旋转
    1. android 反射监听手机旋转,旋转就重新创建编码,PC端检测手机传过来的编码变化就要重新处理buffer,我开始不知道,所以旋转导致ffmepeg解码报错,我都不知道什么原因。

      总结

      上面基本是scrcpy基本逻辑需要技术点,下篇文章我会说怎么找到adb 命令对象实现代码。
      这篇文章只是从我大脑技术一下子写完,总结自己疑惑。

      写博客只是养成一个长期习惯,从而实现思考的复利,博客加广告只是看是不是有真人看我的博客,其实有没有都不是那么重要(有更好),现在总共靠广告赚了3美元了,100美元提现,感觉这辈子应该看不到了。。

如果我的博客对你真有帮助,那我也稍微欣慰一下。。。
突然想起来,我当初写博客只是想看一下,互联网从0到1链接,感受互联的感觉。目前已经感受到了,发现google搜索带过来量,自己基本没有放过外联。不知道他怎么跑过来的。

android app_process 反射实现广播

背景

因为Scrcpy 目前版本不支持输入法,自己的协同工具为了方便自己使用,增加直接在windows用输入法,而不是直接Keycode方式,这样子体验好很多。

使用输入法

我这里直接用了网上开源 ADBKeyBoard 解决 不能支持uncode编码问题(因为adb input 不支持unicode,如果直接反射也是不可以使用的【这样子中文就有很大问题】),但这个实现用广播,原来scrcpy没有反射广播,于是我看 adb shell am broadcast -a 对应在android cmd 里面目录代码,看他用了broadcastIntent 方法

反射 Android12 broadcastIntent

public void sendBroadcast(Intent intent){
        //IntentReceiver receiver = new IntentReceiver();

        try {
            Class cls = Class.forName("android.app.IApplicationThread");
            //   public int broadcastIntent(IApplicationThread caller,
            //2352            Intent intent, String resolvedType,  IIntentReceiver resultTo,
            //2353            int resultCode, String resultData, Bundle map,
            //2354            String requiredPermission, int appOp, boolean serialized,
            //2355            boolean sticky, int userId)
            Method method = manager.getClass().getMethod("broadcastIntent",cls, Intent.class, String.class, IIntentReceiver.class, int.class, String.class,
                    Bundle.class, String[].class, int.class, Bundle.class,boolean.class, boolean.class, int.class);

            Object objString = method.invoke(manager, null,intent, null, null, 0, null, null, null, -1, null, true, false, android.os.Process.myUid()/100000);
            Ln.i("调用完成:" + objString.toString() );
        } catch (ClassNotFoundException e) {
            Ln.e("notfound" + e.toString());
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            Ln.e("InvocationTargetException" + e.toString());
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            Ln.e("NoSuchMethodException" + e.toString());
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            Ln.e("IllegalAccessException" + e.toString());
            e.printStackTrace();
        }
        //manager.getClass().getMethod()
        /*this.broadcastIntent(null, intent, null, null, 0, null, null, null, true, false,
                Binder.getOrigCallingUser());*/
        //receiver.waitForFinish();
    }

这个参数挺多的,不能版本参数不一样,所以要看android的源代码,这样子我在windows 启动SDL 输入法
响应input

        if (event.type == SDL_TEXTINPUT) {
            HandleInput(event); //直接发送网络请求到android里面,然后调用反射发送广播
            continue;
        }

这样子就是简单实现app_process 反射广播

难点

  1. 一开始不不知道怎么弄,所以要看adb 里面发送广播的实现,scrcpy 实现本质也是看android adb源代码,所以协同工具本质就是自己实现一个adb在 app_process,同时app_process 启动进程使用shell权限,所以能反射一些高级命令,模拟点击就是具有adb权限,如果apk 具备这样子权限,那么也可以直接反射调用点击命令。
  2. 参数好多,如果不熟练很容易反射报错。今天我实现反射shellCommand 命令(高版本android 实现命令新代码),实例化带一个参数报错,结果我传入null,默认转换空参数数组,导致实例化失败。。
  3. 不同android版本反射参数有区别,我这里只是反射自己用的,所以要注意。

补充

后面可以说一下Scrcpy 开发思路,同时重点说一下服务怎么反射

scrcpy 为什么要在旋转停止发送视频流

问题

我自己写了电脑控制手机协同(借鉴Scrcpy),但是自己偶尔会出现花屏(屏幕会出现残影)

排查

1:通过错误我找到无效数据包,但不知道为什么会无效,我把pc c++代码看了一下与sdl官方代码对比一下修复几个地方可能导致丢包的逻辑。

2:我发现屏幕旋转后,然后回到桌面,很容易出现花屏,因为我看过scrcpy代码,我发现他旋转时候会停止发送数据包,而我是没有处理

while (!consumeRotationChange() && !eof) {
            //这里是一直等待,所以这里很容易出现卡住,然后buffeer
            //是之前的surface的,旋转就导致宽度和高度不一样,导致前后不对
            int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
            eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
            try {
                if (consumeRotationChange()) {
                    // must restart encoding with new size
                    //这里跳出来,
                    break;
                }
                if (outputBufferId >= 0) {
                    ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);

                    if (sendFrameMeta) {
                        writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
                    }

                    IO.writeFully(fd, codecBuffer);
                    if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
                        // If this is not a config packet, then it contains a frame
                        firstFrameSent = true;
                    }
                }
            } finally {
                if (outputBufferId >= 0) {
                    codec.releaseOutputBuffer(outputBufferId, false);
                }
            }
        }

旋转会重新创建surface,这样子采集的数据不一样,所以会出现数据包错误。。。

总结

很多时候有现成的产品作为参考,可以避免很多坑,但如果自己不写,为什么这么是不知道真正的原因的。

浏览器自动删除指定网址历史记录插件

背景

自己访问一些网址,关闭标签或者关闭浏览器不会出现历史记录里面。

 插件

DynamicHistory

在chrome商店可以找到。

https://chrome.google.com/webstore/detail/dynamichistory/ehkdegpnplleadlmjoaidmjiabocgpok

https://www.chajianxw.com/product-tool/42001.html
这个是国内访问,我没有下载,只是搜到了,你可以下载试试,具体安装方法都是通过开发者模式进行安装,这种扩展网站都有写怎么安装的,你按照图片就可以了。

使用


点击插件,然后在上面框输入网址,我用* ,代表任意google的网址都不会记录。

其他的样式,自己可以在插件选项设置,主要是在显示被历史删除的网址的样式,标题是不一样的。
我自己都去掉了,这个看自己喜好