Android PC投屏簡單嘗試—最終章2

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

上一章中,我們簡單實現(xiàn)了PC的投屏功能。
但是還是存在這一些缺陷。

  1. 屏幕的尺寸數(shù)據(jù)是寫死的
  2. 不能通過PC來對手機進行控制
  3. 直接在主線程中進行解碼和顯示,存在較大的延遲。

所以這邊文章。我們需要根據(jù)上面的需求。來對我們的代碼進行優(yōu)化。

1. 屏幕信息發(fā)送

其實在上一章中,我們已經(jīng)獲取了屏幕信息。只是沒有發(fā)送給client端。這邊文章中,我們進行發(fā)送。

  • android端
    Android端在Socket連接成功后,就開啟發(fā)送
    private static void sendScreenInfo(Size size, ByteBuffer buffer, FileDescriptor fileDescriptor) throws IOException {
        //將尺寸數(shù)據(jù)先發(fā)送過去
        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("發(fā)送尺寸 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("發(fā)送尺寸 size result ");
        buffer.clear();
    }
  • Client端
    在PC上負責接受,并設置給編碼器
  //從客戶端接受屏幕數(shù)據(jù)
    uint8_t size[4];
    socketConnection->recv_from_(reinterpret_cast<uint8_t *>(size), 4);

    //這里先寫死,后面從客戶端內(nèi)接受
    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};
}

//調(diào)用
void set(){
 struct size frame_size = {
            .height=screen_h,
            .width=screen_w
    };
    struct size window_size = get_optimal_size(frame_size, frame_size);

    //創(chuàng)建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發(fā)起模擬按鍵。

  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實現(xiàn)
    我們這里也是通過這個方式來實現(xiàn)的。

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。

  • 創(chuàng)建KeyEvent
public class KeyEventFactory {
    /*
    創(chuàng)建一個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. 事件發(fā)生的時間 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. 重復的事件次數(shù)。點出次數(shù)? 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)};
    }
}
  • 創(chuàng)建MotionEvent
    Android中的手勢事件的觸發(fā)。
 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
    最后是調(diào)用注入該事件
    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端)發(fā)送事件

通過SDL2的事件循環(huán)來監(jiān)聽,對輸入的事件進行相應

開啟事件循環(huán)

需要注意的是:

  1. 必須在主線程內(nèi)(main方法所在的線程內(nèi))開啟事件循環(huán)
    否則分分鐘給你一個異常。
  2. 開啟事件循環(huán)后,窗口上就出現(xiàn)按鈕了


    開啟事件循環(huán)前

    開啟事件循環(huán)后出現(xiàn)窗口上的按鈕.png

開啟事件循環(huán)代碼

  //開啟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發(fā)送

//對應點擊事件
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) {
        //發(fā)送down 事件
        buf[1] = 1;
    } else {
        // 發(fā)送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;

    //繼續(xù)滾動距離
    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) {
        //這個時候發(fā)送的是按下的狀態(tài)
        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);
        }
    }
}

這里可以看到,根據(jù)每一種事件,都定義了對應的方式進行發(fā)送。那Android端,可以通過對應的方式進行接收就可以了~

  • Server端(Android端)接收事件
    接收client端發(fā)送的事件。將其解析,注入
         do {
                //讀到數(shù)據(jù)
                int read = Os.read(fileDescriptor, buffer);
                System.out.println("read=" + read + ",position=" + buffer.position() + "," +
                        "limit=" + buffer.limit() + ",remaining " + buffer.remaining());
                //當讀到的長度為0,就結(jié)束了。
                if (read == -1 || read == 0) {
                    //如果這個時候read 0 的話。就結(jié)束
                    break;
                } else {
                    buffer.flip();
                    //上面定義的,如果是按鈕事件,第一個必須是0
                    byte b = buffer.get(0);
                    //進入對應的事件
                    if (b == 0 && read > 1) { //如果是0 的話,就當作是Action
                        //第2個是判斷事件的類型
                        byte type = buffer.get(1);
                       //按鍵事件。它發(fā)送時定義的長度是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);

這樣就可以進行事件的相應了。

顯示和處理事件的優(yōu)化

梳理優(yōu)化邏輯

  1. 解碼線程異步
    雖然我們已經(jīng)通過Android的Api實現(xiàn)了按鍵注入,并且定義了Socket兩端對按鍵通信的協(xié)議。但是我們之前將解碼的循環(huán)已經(jīng)寫在主線程中了。這樣我們需要將事件的循環(huán)加入到主線程中,才能對事件發(fā)起響應。
    所以我們需要為我們的解碼循環(huán),創(chuàng)建一個解碼線程,在異步進行解碼。
  2. Socket通信異步
    同時,和上一章相同,結(jié)合我們豐富的開發(fā)經(jīng)驗知道,我們不能將耗時任務,放在主線程當中。所以事件通信。我們也需要放到異步處理。
  3. 隊列操作
    我們知道事件循環(huán)會源源不斷的送入,而我們的事件發(fā)送只能一個一個的發(fā)送。所以我們需要為事件循環(huán)加入隊列的緩存。從主線程中接受事件,從發(fā)送線程中,對隊列中的事件進行一個一個的處理。
    同時,根據(jù)之前的學習,我們也知道,我們的ffmpeg解碼和顯示其實也應該加入隊列顯示。這樣我們就可以防止丟幀的存在。
    但是我們這里為了簡單顯示,只是緩存了兩幀。
    一幀負責送顯。一幀負責接受解碼的幀。

線程模型

優(yōu)化后的線程模型如下:

- client端(PC)
    - event_loop
        SDL的EventLoop。復制渲染上屏和分發(fā)事件
    - event_sender(Socket send)
        接受SDL分發(fā)的事件。并把對應的事件通過Socket分發(fā)給Android手機。
    - screen_receiver(Socket recv)
        通過Socket接受的 H264 Naul,使用FFmpeg進行解碼。

- server端(Android)
    - screen record (Socket InputStream)
        使用SurfaceControl和MediaCodec進行屏幕錄制,錄制的結(jié)果通過Socket發(fā)送
    - event_loop (Socket OutputStream)
        接受Socket發(fā)送過來的事件。并調(diào)用對應的API進行事件的注入(InputManager)

### 線程通信
- frames
兩塊緩存區(qū)域。
   - decode_frame
        解碼放置的frame
   - render_frame
        渲染需要的frame.使用該frame 進行render
數(shù)據(jù)流動
   - 生產(chǎn)的過程
     screen_receiver 負責生產(chǎn)。
   - 消費的過程
     event_loop 負責消費。將兩塊緩存區(qū)域進行交換,并把render_frame上屏

- event
一個event_queue隊列來接受??梢允褂面湵?數(shù)據(jù)流動
   - 生產(chǎn)的過程
     event_loop 負責生產(chǎn)。并把數(shù)據(jù)送入隊列當中
   - 消費的過程
     event_sender 負責消費。如果隊列不為空,則進行發(fā)送

這里就不詳細說明了。具體可以看代碼就明白了。

最后的結(jié)果

最后的結(jié)果.gif

就和Vysorscrcpy一樣,我們可以通過投屏PC ,并操作手機了。而且在很低的延遲下。

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

還有更多的細節(jié)處理,可以參考scrcpy

總結(jié)

Android PC投屏簡單嘗試 這一系列文章,終于到了尾聲。總共橫跨了大半年的事件。
最后分成下面幾個方面來進行一下總結(jié)

數(shù)據(jù)源

截屏數(shù)據(jù)的獲取
  1. Android的MediaProjection API
    通過MediaProjection的權限的獲取和調(diào)用其API就能創(chuàng)建一個屏幕的錄制屏幕
  2. 直接反射調(diào)用SurfaceControl的系列方法
    因為在app_process下,我們有較高的權限。所以可以直接通過反射調(diào)用SurfaceControl
    的方法,來完成錄制屏幕數(shù)據(jù)的獲取。(參考adb screenrecord 命令)
截屏數(shù)據(jù)的處理
  1. MediaCodec硬件編碼
    使用MediaCodec結(jié)合Surface ,能容易就能得到編碼后的H264數(shù)據(jù)。
  2. 使用ImageReader的方式。
    使用ImageReader 的方式,可以獲取一幀一幀的數(shù)據(jù)。之后我們可以選擇直接發(fā)送Bitmap數(shù)據(jù)。或者結(jié)合自己的軟件解碼器(FFmpeg或者X264)來編碼獲得H264數(shù)據(jù)。

發(fā)送的協(xié)議

自己定義的Socket協(xié)議

就是適合簡單的發(fā)送Bitmap。只要接受端能夠解析這個bitmap數(shù)據(jù),就可以完成數(shù)據(jù)的展示。

RTMP協(xié)議

可以通過在服務端建立RTMP協(xié)議,然后通過這個協(xié)議進行。使用RTMP協(xié)議發(fā)送的好處在于,需要播放的端只要支持該協(xié)議,就可以輕松的進行拉流播放。

通過USB和ADB協(xié)議進行連接

這個僅僅適合于PC能夠直接用ADB和手機連接的場景。
但是在這個場景下,投屏的效果清晰,流暢,延遲很低。
暫時部分,因為直接發(fā)送H264數(shù)據(jù),只要進行解碼后,就可以進行播放了。(文章使用了SDL2的方式進行了方便的播放。)

知識點

整個過程中
我們對Media Codec和ImageReader/RTMP協(xié)議/FFmpeg/SDL2/Gradle進行了知識點的串聯(lián)。
其實還是挺好玩的。

另外

如果是需要改成手機和手機連接。我們要怎么實現(xiàn)呢?
其實從上面不難看出。如果是手機和手機連接。
在近距離,我們可以簡單的使用藍牙進行Socket(類似ADB和USB的通信方式)。
如果是遠距離,就可以通過RMTP的方式,來進行推流和拉流。

最后,完結(jié)撒花??~~

投屏嘗試系列文章

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容