美文网首页精选案例
Android PC投屏简单尝试—最终章2

Android PC投屏简单尝试—最终章2

作者: deep_sadness | 来源:发表于2018-12-24 21:14 被阅读139次

    源码地址:https://github.com/deepsadness/AppRemote

    上一章中,我们简单实现了PC的投屏功能。
    但是还是存在这一些缺陷。

    1. 屏幕的尺寸数据是写死的
    2. 不能通过PC来对手机进行控制
    3. 直接在主线程中进行解码和显示,存在较大的延迟。

    所以这边文章。我们需要根据上面的需求。来对我们的代码进行优化。

    1. 屏幕信息发送

    其实在上一章中,我们已经获取了屏幕信息。只是没有发送给client端。这边文章中,我们进行发送。

    • android端
      Android端在Socket连接成功后,就开启发送
        private static void sendScreenInfo(Size size, ByteBuffer buffer, FileDescriptor fileDescriptor) throws IOException {
            //将尺寸数据先发送过去
            int width = size.getWidth();
            int height = size.getHeight();
            byte wHigh = (byte) (width >> 8);
            byte wLow = (byte) (width & 0xff);
    
            byte hHigh = (byte) (height >> 8);
            byte hLow = (byte) (height & 0xff);
    
            buffer.put(wHigh);
            buffer.put(wLow);
    
            buffer.put(hHigh);
            buffer.put(hLow);
    
    //            System.out.println("发送尺寸 size result = " + write);
    //            int write = Os.write(fileDescriptor, buffer);
            byte[] buffer_size = new byte[4];
            buffer_size[0] = (byte) (width >> 8);
            buffer_size[1] = (byte) (width & 0xff);
            buffer_size[2] = (byte) (height >> 8);
            buffer_size[3] = (byte) (height & 0xff);
            writeFully(fileDescriptor, buffer_size, 0, buffer_size.length);
            System.out.println("发送尺寸 size result ");
            buffer.clear();
        }
    
    • Client端
      在PC上负责接受,并设置给编码器
      //从客户端接受屏幕数据
        uint8_t size[4];
        socketConnection->recv_from_(reinterpret_cast<uint8_t *>(size), 4);
    
        //这里先写死,后面从客户端内接受
        int width = (size[0] << 8) | (size[1]);
        int height = (size[2] << 8) | (size[3]);
    
        printf("width = %d , height = %d \n", width, height);
    

    这样就可以获得屏幕的尺寸信息,保证不同手机分辨率也能正常使用了。

    • 奇怪的地方


      有点胖.png

    尽管我们通过这样获取了正确的屏幕信息,但是SDL显示的画面,还是有些奇怪。比我们预期的胖了一点。

    通过下面的方式,来重新计算窗口的尺寸。这样才能显示正常。

    //这里是给四周留空隙。
    #define DISPLAY_MARGINS 96
    struct size {
        int width;
        int height;
    };
    // get the preferred display bounds (i.e. the screen bounds with some margins)
    static SDL_bool get_preferred_display_bounds(struct size *bounds) {
        SDL_Rect rect;
    #if SDL_VERSION_ATLEAST(2, 0, 5)
    # define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayUsableBounds((i), (r))
    #else
    # define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayBounds((i), (r))
    #endif
        //获取显示的大小
        if (GET_DISPLAY_BOUNDS(0, &rect)) {
    //        LOGW("Could not get display usable bounds: %s", SDL_GetError());
            printf("Could not get display usable bounds: %s\n", SDL_GetError());
            return SDL_FALSE;
        }
        //设置大小
        bounds->width = MAX(0, rect.w - DISPLAY_MARGINS);
        bounds->height = MAX(0, rect.h - DISPLAY_MARGINS);
        return SDL_TRUE;
    }
    
    // return the optimal size of the window, with the following constraints:
    //  - it attempts to keep at least one dimension of the current_size (i.e. it crops the black borders)
    //  - it keeps the aspect ratio
    //  - it scales down to make it fit in the display_size
    static struct size get_optimal_size(struct size current_size, struct size frame_size) {
        if (frame_size.width == 0 || frame_size.height == 0) {
            // avoid division by 0
            return current_size;
        }
    
        struct size display_size;
        // 32 bits because we need to multiply two 16 bits values
        int w;
        int h;
    
        if (!get_preferred_display_bounds(&display_size)) {
            // cannot get display bounds, do not constraint the size
            w = current_size.width;
            h = current_size.height;
        } else {
            w = MIN(current_size.width, display_size.width);
            h = MIN(current_size.height, display_size.height);
        }
    
        SDL_bool keep_width = static_cast<SDL_bool>(frame_size.width * h > frame_size.height * w);
      //缩放之后,保持长宽比
        if (keep_width) {
            // remove black borders on top and bottom
            h = frame_size.height * w / frame_size.width;
        } else {
            // remove black borders on left and right (or none at all if it already fits)
            w = frame_size.width * h / frame_size.height;
        }
    
        // w and h must fit into 16 bits
        SDL_assert_release(w < 0x10000 && h < 0x10000);
        return (struct size) {w, h};
    }
    
    //调用
    void set(){
     struct size frame_size = {
                .height=screen_h,
                .width=screen_w
        };
        struct size window_size = get_optimal_size(frame_size, frame_size);
    
        //创建window
        sdl_window = SDL_CreateWindow(
                name,
                SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
                window_size.width, window_size.height,
                SDL_WINDOW_RESIZABLE);
    }
    

    这样才能显示正常的窗口了。


    正常的比例.png

    2. 对Android手机进行控制

    我们知道在Android中有几种方式可以对手机的Android发起模拟按键。

    1. 通过AccessibilityService的方式。通过注册该服务,可以捕获所有的窗口变化,捕获控键,进行模拟点击。
      但是它需要额外的权限。
    2. 通过adb的方式
      我们可以简单的通过adb shell input方法来完成模拟
    Usage: input [<source>] <command> [<arg>...]
    
    The sources are: 
          dpad
          keyboard
          mouse
          touchpad
          gamepad
          touchnavigation
          joystick
          touchscreen
          stylus
          trackball
    
    The commands and default sources are:
          text <string> (Default: touchscreen)
          keyevent [--longpress] <key code number or name> ... (Default: keyboard)
          tap <x> <y> (Default: touchscreen)
          swipe <x1> <y1> <x2> <y2> [duration(ms)] (Default: touchscreen)
          draganddrop <x1> <y1> <x2> <y2> [duration(ms)] (Default: touchscreen)
          press (Default: trackball)
          roll <dx> <dy> (Default: trackball)
    

    就可以对屏幕上(100,100)的位置,进行模拟点击。

    1. 通过InputManager实现
      我们这里也是通过这个方式来实现的。

    InputManager 模拟点击事件

    当API 15之后,我们使用InputManager。

    • 获取InputManager
      同样可以通过Server Manager中就可以进行获取。
      public InputManager getInputManager() {
            if (inputManager == null) {
                IInterface service = getService(Context.INPUT_SERVICE, "android.hardware.input.IInputManager");
                inputManager = new InputManager(service);
            }
            return inputManager;
        }
    

    我们知道Android中的按键事件对应的是KeyEvent,而手势事件对应的是MotionEvent

    • 创建KeyEvent
    public class KeyEventFactory {
        /*
        创建一个KeyEvent
         */
        public static KeyEvent keyEvent(int action, int keyCode, int repeat, int metaState) {
            long now = SystemClock.uptimeMillis();
            /**
             * 1. 点击的时间 The time (in {@link android.os.SystemClock#uptimeMillis}) at which this key code originally went down.
             * 2. 事件发生的时间 The time (in {@link android.os.SystemClock#uptimeMillis}) at which this event happened.
             * 3. UP DOWN MULTIPLE 中的一个: either {@link #ACTION_DOWN},{@link #ACTION_UP}, or {@link #ACTION_MULTIPLE}.
             * 4. code The key code. 输入的键盘事件
             * 5. 重复的事件次数。点出次数? A repeat count for down events (> 0 if this is after the initial down) or event count for multiple events.
             * 6. metaState Flags indicating which meta keys are currently pressed.  暂时不知道什么意思
             * 7. The device ID that generated the key event.
             * 8. Raw device scan code of the event. 暂时不知道什么意思
             * 9. The flags for this key event 暂时不知道什么意思
             * 10. The input source such as {@link InputDevice#SOURCE_KEYBOARD}.
             */
            KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState,
                    KeyCharacterMap.VIRTUAL_KEYBOARD,
                    0,
                    0,
                    InputDevice.SOURCE_KEYBOARD);
            return event;
        }
    
        /*
        通过送入一个ACTION_DOWN 和ACTION_UP 来模拟一次点击的事件
         */
        public static KeyEvent[] clickEvent(int keyCode) {
            return new KeyEvent[]{keyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0)
                    , keyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0)};
        }
    }
    
    • 创建MotionEvent
      Android中的手势事件的触发。
     private static long lastMouseDown;
        private static final MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()};
        private static final MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent
                .PointerProperties()};
    
        public static MotionEvent createMotionEvent(int type, int x, int y) {
            long now = SystemClock.uptimeMillis();
            int action;
            if (type == 1) {
                lastMouseDown = now;
                action = MotionEvent.ACTION_DOWN;
            } else {
                action = MotionEvent.ACTION_UP;
            }
            MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()};
            MotionEvent.PointerCoords coords = pointerCoords[0];
            coords.x = 2 * x;
            coords.y = 2 * y;
            MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent
                    .PointerProperties()};
            MotionEvent.PointerProperties props = pointerProperties[0];
            props.id = 0;
            props.toolType = MotionEvent.TOOL_TYPE_FINGER;
    
            coords = pointerCoords[0];
            coords.orientation = 0;
            coords.pressure = 1;
            coords.size = 1;
    
            return MotionEvent.obtain(
                    lastMouseDown, now,
                    action,
                    1, pointerProperties, pointerCoords,
                    0, 1,
                    1f, 1f,
                    0, 0,
                    InputDevice.SOURCE_TOUCHSCREEN, 0);
        }
    
    • 滚动手势
    public static MotionEvent createScrollEvent(int x, int y, int hScroll, int vScroll) {
            long now = SystemClock.uptimeMillis();
    
            MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()};
            MotionEvent.PointerCoords coords = pointerCoords[0];
            coords.x = 2 * x;
            coords.y = 2 * y;
            MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent
                    .PointerProperties()};
            MotionEvent.PointerProperties props = pointerProperties[0];
            props.id = 0;
            props.toolType = MotionEvent.TOOL_TYPE_FINGER;
    
            coords = pointerCoords[0];
            coords.orientation = 0;
            coords.pressure = 1;
            coords.size = 1;
            coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
            coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
            return MotionEvent.obtain(lastMouseDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, 0,
                    0, InputDevice.SOURCE_MOUSE, 0);
        }
    
    • 注入Event
      最后是调用注入该事件
        public boolean injectInputEvent(InputEvent inputEvent, int mode) {
            try {
                return (Boolean) injectInputEventMethod.invoke(service, inputEvent, mode);
            } catch (InvocationTargetException | IllegalAccessException e) {
                e.printStackTrace();
                throw new AssertionError(e);
            }
        }
    

    值得注意的是:一次点击事件是由一个DOWN 和UP事件组成的。

    进行通信

    Client端(PC端)发送事件

    通过SDL2的事件循环来监听,对输入的事件进行相应

    开启事件循环

    需要注意的是:

    1. 必须在主线程内(main方法所在的线程内)开启事件循环
      否则分分钟给你一个异常。
    2. 开启事件循环后,窗口上就出现按钮了


      开启事件循环前
      开启事件循环后出现窗口上的按钮.png

    开启事件循环代码

      //开启Event Loop
        for (;;) {
            SDL_WaitEvent(&event);
            //这里我们主要相应了
            if (event.type == SDL_MOUSEBUTTONDOWN) {  //点击事件的DOWN
                handleButtonEvent(sc, &event.button);
            } else if (event.type == SDL_MOUSEBUTTONUP) { //点击事件的UP
                handleButtonEvent(sc, &event.button);
            } else if (event.type == SDL_KEYDOWN) {  //按键事件DOWN
                handleSDLKeyEvent(sc, &event.key);
            } else if (event.type == SDL_KEYUP) { //按键事件UP
                handleSDLKeyEvent(sc, &event.key);
            } else if (event.type == SDL_MOUSEWHEEL) {  // 滚轮事件
                //处理滑动事件
                handleScrollEvent(sc, &event.wheel);
            } else if (event.type == SDL_QUIT) {  // 点击窗口上的关闭按钮
                printf("rev event type=SDL_QUIT\n");
                sc->destroy();
                break;
            } 
    

    事件处理代码
    其实就是将这些事件解析成坐标,然后通过socket发送

    //对应点击事件
    void handleButtonEvent(SDL_Screen *screen, SDL_MouseButtonEvent *event) {
        int width = screen->screen_w;
        int height = screen->screen_h;
        int x = event->x;
        int y = event->y;
        //是否超过来边界
        bool outside_device_screen = x < 0 || x >= width ||
                                     y < 0 || y >= height;
    
        if (event->type == SDL_MOUSEBUTTONDOWN) {
        }
    
        printf("outside_device_screen =%d\n", outside_device_screen);
        if (outside_device_screen) {
            // ignore
            return;
        }
        char buf[6];
        memset(buf, 0, sizeof(buf));
        printf("event x =%d\n", event->x);
        printf("event y =%d\n", event->y);
        printf("event char size =%zu\n", sizeof(char));
        buf[0] = 0;
        if (event->type == SDL_MOUSEBUTTONDOWN) {
            //发送down 事件
            buf[1] = 1;
        } else {
            // 发送UP事件
            buf[1] = 0;
        }
        //高8位
        buf[2] = event->x >> 8;
        //低8位
        buf[3] = event->x & 0xff;
        //高8位
        buf[4] = event->y >> 8;
        //低8位
        buf[5] = event->y & 0xff;
    
        int result = send(client_event, buf, 6, 0);
        printf("send result = %d\n", result);
    }
    
    //  对应滑动事件
    // Convert window coordinates (as provided by SDL_GetMouseState() to renderer coordinates (as provided in SDL mouse events)
    //
    // See my question:
    // <https://stackoverflow.com/questions/49111054/how-to-get-mouse-position-on-mouse-wheel-event>
    void handleScrollEvent(SDL_Screen *sc, SDL_MouseWheelEvent *event) {
        //处理滑动事件
        int x_c;
        int y_c;
        int *x = &x_c;
        int *y = &y_c;
        SDL_GetMouseState(x, y);
        SDL_Rect viewport;
        float scale_x, scale_y;
        SDL_RenderGetViewport(sc->sdl_renderer, &viewport);
        SDL_RenderGetScale(sc->sdl_renderer, &scale_x, &scale_y);
        *x = (int) (*x / scale_x) - viewport.x;
        *y = (int) (*y / scale_y) - viewport.y;
    
    
        int width = sc->screen_w;
        int height = sc->screen_h;
    
        //是否超过来边界
        bool outside_device_screen = x_c < 0 || x_c >= width ||
                                     y_c < 0 || y_c >= height;
    
        printf("outside_device_screen =%d\n", outside_device_screen);
        if (outside_device_screen) {
            // ignore
            return;
        }
    
        SDL_assert_release(x_c >= 0 && x_c < 0x10000 && y_c >= 0 && y_c < 0x10000);
    
        //使用这个来记录滑动的方向
        // SDL behavior seems inconsistent between horizontal and vertical scrolling
        // so reverse the horizontal
        // <https://wiki.libsdl.org/SDL_MouseWheelEvent#Remarks>
        // SDL 的滑动情况,两个方向不一致
        int mul = event->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1;
        int hs = -mul * event->x;
        int vs = mul * event->y;
    
        char buf[14];
        memset(buf, 0, sizeof(buf));
        printf(" x_c =%d\n", x_c);
        printf(" y_c =%d\n", y_c);
        printf(" hs =%d\n", hs);
        printf(" vs =%d\n", vs);
        buf[0] = 0;
        //滚动事件
        buf[1] = 2;
        //高8位
        buf[2] = x_c >> 8;
        //低8位
        buf[3] = x_c & 0xff;
        //高8位
        buf[4] = y_c >> 8;
        //低8位
        buf[5] = y_c & 0xff;
    
        //继续滚动距离
        buf[6] = hs >> 24;
        //低8位
        buf[7] = hs >> 16;
        buf[8] = hs >> 8;
        buf[9] = hs;
    
    
        //高8位
        buf[10] = vs >> 24;
        //低8位
        buf[11] = vs >> 16;
        buf[12] = vs >> 8;
        buf[13] = vs;
    
        int result = send(client_event, buf, 14, 0);
        printf("send result = %d\n", result);
    
    }
    
    //对应键盘上的按钮事件。
    void handleSDLKeyEvent(SDL_Screen *sc, SDL_KeyboardEvent *event) {
        //分别对应 mac 上的 control option command
        int ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL);
        int alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT);
        int meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI);
        printf("ctrl = %d,", ctrl);
        printf("meta = %d,", meta);
        printf("alt = %d,\n", alt);
    
        ////因为我是mac键盘,期望control+ H = home键 control+b = back键
        //再去取keycode
        SDL_Keycode keycode = event->keysym.sym;
        printf("keycode = %d, action type = %d\n", keycode, event->type);
        printf("b = %d, action type = %d\n", SDLK_b, event->type);
        if (event->type == SDL_KEYDOWN && ctrl != 0) {
            //这个时候发送的是按下的状态
            if (keycode == SDLK_h) {
                char buf[4];
                memset(buf, 0, sizeof(buf));
                buf[0] = 0;
                //自定义的案件事件
                buf[1] = 3;
                //1 是 down
                buf[2] = 1;
                //key code home 键对应的是 3
                buf[3] = 3;
                int result = send(client_event, buf, 4, 0);
                printf("send result = %d\n", result);
            } else if (keycode == SDLK_b) {
                char buf[4];
                memset(buf, 0, sizeof(buf));
                buf[0] = 0;
                //自定义的案件事件
                buf[1] = 3;
                //1 是 down
                buf[2] = 1;
                //key code back 键对应的是 4
                buf[3] = 4;
                int result = send(client_event, buf, 4, 0);
                printf("send result = %d\n", result);
            }
        }
        if (event->type == SDL_KEYUP && keycode != 0) {
            if (keycode == SDLK_h) {
                char buf[4];
                memset(buf, 0, sizeof(buf));
                buf[0] = 0;
                //自定义的案件事件
                buf[1] = 3;
                //1 是 up
                buf[2] = 0;
                //key code home 键对应的是 3
                buf[3] = 3;
                int result = send(client_event, buf, 4, 0);
                printf("send result = %d\n", result);
            } else if (keycode == SDLK_b) {
                char buf[4];
                memset(buf, 0, sizeof(buf));
                buf[0] = 0;
                //自定义的案件事件
                buf[1] = 3;
                //1 是 up
                buf[2] = 0;
                //key code back 键对应的是 4
                buf[3] = 4;
                int result = send(client_event, buf, 4, 0);
                printf("send result = %d\n", result);
            }
        }
    }
    

    这里可以看到,根据每一种事件,都定义了对应的方式进行发送。那Android端,可以通过对应的方式进行接收就可以了~

    • Server端(Android端)接收事件
      接收client端发送的事件。将其解析,注入
             do {
                    //读到数据
                    int read = Os.read(fileDescriptor, buffer);
                    System.out.println("read=" + read + ",position=" + buffer.position() + "," +
                            "limit=" + buffer.limit() + ",remaining " + buffer.remaining());
                    //当读到的长度为0,就结束了。
                    if (read == -1 || read == 0) {
                        //如果这个时候read 0 的话。就结束
                        break;
                    } else {
                        buffer.flip();
                        //上面定义的,如果是按钮事件,第一个必须是0
                        byte b = buffer.get(0);
                        //进入对应的事件
                        if (b == 0 && read > 1) { //如果是0 的话,就当作是Action
                            //第2个是判断事件的类型
                            byte type = buffer.get(1);
                           //按键事件。它发送时定义的长度是6
                            if (type < 2 && read == 6) {//action down 1 down 0 up
                                System.out.println("enter key event");
                                buffer.position(1);
                                int x = buffer.get(2) << 8 | buffer.get(3) & 0xff;
                                int y = buffer.get(4) << 8 | buffer.get(5) & 0xff;
                                //接受到事件进行处理
                                boolean key = createKey(serviceManager, type, x, y);
                                buffer.clear();
                            } else if (type == 2 && read == 14) { //滚动事件.定义的长度是14
                                buffer.position(1);
                                //x,y是接触的点,hs是水平的滑动,vs 是上下的滑动
                                int x = buffer.get(2) << 8 | buffer.get(3) & 0xff;
                                int y = buffer.get(4) << 8 | buffer.get(5) & 0xff;
                                int hs = buffer.get(6) << 24 | buffer.get(7) << 16 | buffer.get(8) <<
                                        8 | buffer.get(9);
    
                                int vs = buffer.get(10) << 24 | buffer.get(11) << 16 | buffer.get(12) <<
                                        8 | buffer.get(13);
                                //接受到事件进行处理
                                boolean b1 = injectScroll(serviceManager, x, y, hs, vs);
                                // 处理完,记得清楚buffer
                                buffer.clear();
                            } else if (type == 3 && read == 4) { //接受按键事件,长度是4
                                System.out.println("enter key code event");
                                int action = buffer.get(2) == 1 ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP;
                                int keyCode = buffer.get(3);
                                boolean key = injectKeyEvent(serviceManager, action, keyCode);
                                // 处理完,记得清楚buffer
                                buffer.clear();
                            }
                        }
                    }
                } while (!eof);
    

    这样就可以进行事件的相应了。

    显示和处理事件的优化

    梳理优化逻辑

    1. 解码线程异步
      虽然我们已经通过Android的Api实现了按键注入,并且定义了Socket两端对按键通信的协议。但是我们之前将解码的循环已经写在主线程中了。这样我们需要将事件的循环加入到主线程中,才能对事件发起响应。
      所以我们需要为我们的解码循环,创建一个解码线程,在异步进行解码。
    2. Socket通信异步
      同时,和上一章相同,结合我们丰富的开发经验知道,我们不能将耗时任务,放在主线程当中。所以事件通信。我们也需要放到异步处理。
    3. 队列操作
      我们知道事件循环会源源不断的送入,而我们的事件发送只能一个一个的发送。所以我们需要为事件循环加入队列的缓存。从主线程中接受事件,从发送线程中,对队列中的事件进行一个一个的处理。
      同时,根据之前的学习,我们也知道,我们的ffmpeg解码和显示其实也应该加入队列显示。这样我们就可以防止丢帧的存在。
      但是我们这里为了简单显示,只是缓存了两帧。
      一帧负责送显。一帧负责接受解码的帧。

    线程模型

    优化后的线程模型如下:

    - client端(PC)
        - event_loop
            SDL的EventLoop。复制渲染上屏和分发事件
        - event_sender(Socket send)
            接受SDL分发的事件。并把对应的事件通过Socket分发给Android手机。
        - screen_receiver(Socket recv)
            通过Socket接受的 H264 Naul,使用FFmpeg进行解码。
    
    - server端(Android)
        - screen record (Socket InputStream)
            使用SurfaceControl和MediaCodec进行屏幕录制,录制的结果通过Socket发送
        - event_loop (Socket OutputStream)
            接受Socket发送过来的事件。并调用对应的API进行事件的注入(InputManager)
    
    ### 线程通信
    - frames
    两块缓存区域。
       - decode_frame
            解码放置的frame
       - render_frame
            渲染需要的frame.使用该frame 进行render
    数据流动
       - 生产的过程
         screen_receiver 负责生产。
       - 消费的过程
         event_loop 负责消费。将两块缓存区域进行交换,并把render_frame上屏
    
    - event
    一个event_queue队列来接受。可以使用链表
    数据流动
       - 生产的过程
         event_loop 负责生产。并把数据送入队列当中
       - 消费的过程
         event_sender 负责消费。如果队列不为空,则进行发送
    

    这里就不详细说明了。具体可以看代码就明白了。

    最后的结果

    最后的结果.gif
    就和Vysorscrcpy一样,我们可以通过投屏PC ,并操作手机了。而且在很低的延迟下。

    源码地址:https://github.com/deepsadness/AppProcessDemo

    还有更多的细节处理,可以参考scrcpy

    总结

    Android PC投屏简单尝试 这一系列文章,终于到了尾声。总共横跨了大半年的事件。
    最后分成下面几个方面来进行一下总结

    数据源

    截屏数据的获取
    1. Android的MediaProjection API
      通过MediaProjection的权限的获取和调用其API就能创建一个屏幕的录制屏幕
    2. 直接反射调用SurfaceControl的系列方法
      因为在app_process下,我们有较高的权限。所以可以直接通过反射调用SurfaceControl
      的方法,来完成录制屏幕数据的获取。(参考adb screenrecord 命令)
    截屏数据的处理
    1. MediaCodec硬件编码
      使用MediaCodec结合Surface ,能容易就能得到编码后的H264数据。
    2. 使用ImageReader的方式。
      使用ImageReader 的方式,可以获取一帧一帧的数据。之后我们可以选择直接发送Bitmap数据。或者结合自己的软件解码器(FFmpeg或者X264)来编码获得H264数据。

    发送的协议

    自己定义的Socket协议

    就是适合简单的发送Bitmap。只要接受端能够解析这个bitmap数据,就可以完成数据的展示。

    RTMP协议

    可以通过在服务端建立RTMP协议,然后通过这个协议进行。使用RTMP协议发送的好处在于,需要播放的端只要支持该协议,就可以轻松的进行拉流播放。

    通过USB和ADB协议进行连接

    这个仅仅适合于PC能够直接用ADB和手机连接的场景。
    但是在这个场景下,投屏的效果清晰,流畅,延迟很低。
    暂时部分,因为直接发送H264数据,只要进行解码后,就可以进行播放了。(文章使用了SDL2的方式进行了方便的播放。)

    知识点

    整个过程中
    我们对Media Codec和ImageReader/RTMP协议/FFmpeg/SDL2/Gradle进行了知识点的串联。
    其实还是挺好玩的。

    另外

    如果是需要改成手机和手机连接。我们要怎么实现呢?
    其实从上面不难看出。如果是手机和手机连接。
    在近距离,我们可以简单的使用蓝牙进行Socket(类似ADB和USB的通信方式)。
    如果是远距离,就可以通过RMTP的方式,来进行推流和拉流。

    最后,完结撒花🎉~~

    投屏尝试系列文章

    相关文章

      网友评论

        本文标题:Android PC投屏简单尝试—最终章2

        本文链接:https://www.haomeiwen.com/subject/yfctlqtx.html